@agimon-ai/po-git 0.0.0-alpha.1 → 0.0.0-alpha.100

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.
@@ -0,0 +1,2726 @@
1
+ import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineGlobalStore, defineNativePlugin, defineSessionStore, defineWebLifecycle, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
2
+ import { View } from "react-native-unistyles/components/native/View";
3
+ import { Text } from "react-native-unistyles/components/native/Text";
4
+ import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
5
+ 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";
6
+ import { createElement, memo, useCallback, useEffect, useEffectEvent, useMemo, useRef, useState, useSyncExternalStore } from "react";
7
+ import { StyleSheet, useUnistyles } from "react-native-unistyles";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
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
+ deleteBranch: {
18
+ method: "DELETE",
19
+ path: "/branches",
20
+ response: void 0
21
+ },
22
+ openBranchFolder: {
23
+ method: "POST",
24
+ path: "/branches/open",
25
+ response: void 0
26
+ },
27
+ /** Workspace: a top-level worktree session on an existing or new branch. */
28
+ createSession: {
29
+ method: "POST",
30
+ path: "/sessions",
31
+ response: void 0
32
+ },
33
+ /** Workspace: that workspace's saved remote auth, never the token itself. */
34
+ auth: {
35
+ method: "GET",
36
+ path: "/auth",
37
+ query: ["repositoryId"],
38
+ response: void 0
39
+ },
40
+ /** Workspace: replaces that workspace's remote auth. Passkey step-up gated for remote devices. */
41
+ saveAuth: {
42
+ method: "PUT",
43
+ path: "/auth",
44
+ query: ["repositoryId"],
45
+ response: void 0
46
+ },
47
+ /** Session: the files this session's checkout changed against its base. */
48
+ review: {
49
+ method: "GET",
50
+ path: "/review",
51
+ response: void 0
52
+ },
53
+ /** Session: one changed file's hunks. */
54
+ reviewFile: {
55
+ method: "GET",
56
+ path: "/review/file",
57
+ query: ["path"],
58
+ response: void 0
59
+ }
60
+ }, {
61
+ scopes: [
62
+ "global",
63
+ "workspace",
64
+ "session"
65
+ ],
66
+ basePath: "git",
67
+ transport: nativeApiTransport,
68
+ sessionAddress: sessionApiAddress
69
+ });
70
+ //#endregion
71
+ //#region src/extensions/workspaces/(frontend)/_lib/newSessionForm.ts
72
+ const MAX_LISTED = 200;
73
+ /** Local branches first, then remote-only ones, filtered by a case-insensitive substring. */
74
+ function branchOptions(branches, query) {
75
+ const needle = query.trim().toLowerCase();
76
+ const matches = (text) => needle === "" || text.toLowerCase().includes(needle);
77
+ const local = branches.local.filter((branch) => matches(branch.name)).map((branch) => ({
78
+ key: `local:${branch.name}`,
79
+ name: branch.name,
80
+ current: branch.name === branches.current,
81
+ ...branch.checkedOutAt === void 0 ? {} : { busyAt: branch.checkedOutAt }
82
+ }));
83
+ const remote = branches.remote.filter((branch) => matches(`${branch.remote}/${branch.name}`)).map((branch) => ({
84
+ key: `remote:${branch.remote}/${branch.name}`,
85
+ name: branch.name,
86
+ remote: branch.remote,
87
+ current: false
88
+ }));
89
+ return [...local, ...remote].slice(0, MAX_LISTED);
90
+ }
91
+ /** Every base a new branch can start from: the default first, then remote-tracking names, then local branches. */
92
+ function baseOptions(branches) {
93
+ return [.../* @__PURE__ */ new Set([
94
+ ...branches.defaultBase === void 0 ? [] : [branches.defaultBase],
95
+ ...branches.remote.map((branch) => `${branch.remote}/${branch.name}`),
96
+ ...branches.local.map((branch) => branch.name)
97
+ ])];
98
+ }
99
+ /** Why the form cannot be submitted yet, or undefined when it can. */
100
+ function formProblem(state) {
101
+ if (state.mode === "existing") {
102
+ if (state.selected === void 0) return "Pick a branch.";
103
+ if (state.selected.busyAt !== void 0) return `That branch is checked out at ${state.selected.busyAt}.`;
104
+ }
105
+ if (state.mode === "new" && state.newBranch.trim() === "") return "Name the new branch.";
106
+ }
107
+ /** The launch profile choice for any session kind, or undefined when none was made. */
108
+ function launchSelection(state) {
109
+ return state.profile === void 0 ? void 0 : { profile: state.profile };
110
+ }
111
+ /** The worktree create request for the form, or undefined in plain mode. */
112
+ function createRequest(state) {
113
+ const name = state.name.trim();
114
+ const chosen = launchSelection(state);
115
+ const named = {
116
+ ...name === "" ? {} : { name },
117
+ ...chosen === void 0 ? {} : { selection: chosen }
118
+ };
119
+ if (state.mode === "existing" && state.selected !== void 0) return {
120
+ mode: "existing-branch",
121
+ branch: state.selected.name,
122
+ ...state.selected.remote === void 0 ? {} : { remote: state.selected.remote },
123
+ ...named
124
+ };
125
+ if (state.mode === "new") {
126
+ const baseRef = state.baseRef.trim();
127
+ return {
128
+ mode: "new-branch",
129
+ branch: state.newBranch.trim(),
130
+ ...baseRef === "" ? {} : { baseRef },
131
+ ...named
132
+ };
133
+ }
134
+ }
135
+ //#endregion
136
+ //#region src/extensions/workspaces/(frontend)/fill/_components/GitNewSessionSheet.native.tsx
137
+ const MODES = [
138
+ {
139
+ value: "existing",
140
+ label: "Existing"
141
+ },
142
+ {
143
+ value: "new",
144
+ label: "New branch"
145
+ },
146
+ {
147
+ value: "plain",
148
+ label: "No branch"
149
+ }
150
+ ];
151
+ /** The native twin of GitNewSessionDialogView: the same form state and checks, in sheet parts. */
152
+ function GitNewSessionSheetView({ workspaceRoot, branches, loadError = "", initialForm, busy = false, error = "", onCreate }) {
153
+ const [form, setForm] = useState({
154
+ mode: "existing",
155
+ newBranch: "",
156
+ baseRef: "",
157
+ name: "",
158
+ ...initialForm
159
+ });
160
+ const [query, setQuery] = useState("");
161
+ const [basesOpen, setBasesOpen] = useState(false);
162
+ const ready = branches?.repository === true && loadError === "";
163
+ const loading = branches === null && loadError === "";
164
+ const mode = ready || loading ? form.mode : "plain";
165
+ const state = {
166
+ ...form,
167
+ mode,
168
+ baseRef: form.baseRef || branches?.defaultBase || ""
169
+ };
170
+ const problem = formProblem(state);
171
+ const update = (patch) => setForm((current) => ({
172
+ ...current,
173
+ ...patch
174
+ }));
175
+ const options = branches === null ? [] : branchOptions(branches, query);
176
+ const bases = branches === null ? [] : baseOptions(branches);
177
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "New session" }) }), /* @__PURE__ */ jsxs(SheetBody, {
178
+ style: styles$3.body,
179
+ children: [
180
+ /* @__PURE__ */ jsx(Text, {
181
+ style: styles$3.root,
182
+ numberOfLines: 1,
183
+ ellipsizeMode: "middle",
184
+ children: workspaceRoot
185
+ }),
186
+ !ready ? /* @__PURE__ */ jsx(Text, {
187
+ testID: "git-new-session-plain-note",
188
+ style: styles$3.dim,
189
+ children: loading ? "Reading branches..." : loadError || "This workspace is not a git checkout. Start a plain session in this folder."
190
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SegmentedControl, {
191
+ accessibilityLabel: "Session kind",
192
+ options: MODES,
193
+ value: mode,
194
+ disabled: busy,
195
+ onValueChange: (value) => update({ mode: value })
196
+ }), mode === "existing" ? /* @__PURE__ */ jsxs(View, {
197
+ style: styles$3.field,
198
+ children: [
199
+ /* @__PURE__ */ jsx(Label, { children: "Branch" }),
200
+ /* @__PURE__ */ jsx(Input, {
201
+ testID: "git-new-session-branch-search",
202
+ accessibilityLabel: "Find a branch",
203
+ placeholder: "Find a branch",
204
+ autoCapitalize: "none",
205
+ autoCorrect: false,
206
+ value: query,
207
+ onChangeText: setQuery,
208
+ disabled: busy
209
+ }),
210
+ /* @__PURE__ */ jsxs(ScrollView, {
211
+ style: styles$3.list,
212
+ nestedScrollEnabled: true,
213
+ accessibilityLabel: "Branches",
214
+ children: [options.length === 0 ? /* @__PURE__ */ jsx(Text, {
215
+ style: [styles$3.dim, styles$3.pad],
216
+ children: "No matching branches"
217
+ }) : null, options.map((branch, index) => /* @__PURE__ */ jsxs(View, { children: [branch.remote !== void 0 && (index === 0 || options[index - 1]?.remote === void 0) ? /* @__PURE__ */ jsx(Text, {
218
+ style: [
219
+ styles$3.dim,
220
+ styles$3.pad,
221
+ styles$3.divider
222
+ ],
223
+ children: "Remote branches"
224
+ }) : null, /* @__PURE__ */ jsx(ListRow, {
225
+ title: /* @__PURE__ */ jsx(Text, {
226
+ style: styles$3.mono,
227
+ children: branch.remote === void 0 ? branch.name : `${branch.remote}/${branch.name}`
228
+ }),
229
+ description: branch.busyAt !== void 0 ? `Checked out in ${branch.busyAt}` : branch.current ? "Current" : void 0,
230
+ selected: form.selected?.key === branch.key,
231
+ disabled: branch.busyAt !== void 0 || busy,
232
+ disclosure: false,
233
+ onPress: () => update({ selected: branch })
234
+ })] }, branch.key))]
235
+ })
236
+ ]
237
+ }) : mode === "new" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(View, {
238
+ style: styles$3.field,
239
+ children: [/* @__PURE__ */ jsx(Label, { children: "Branch name" }), /* @__PURE__ */ jsx(Input, {
240
+ testID: "git-new-session-new-branch",
241
+ accessibilityLabel: "Branch name",
242
+ placeholder: "feature/my-change",
243
+ autoCapitalize: "none",
244
+ autoCorrect: false,
245
+ style: styles$3.mono,
246
+ value: form.newBranch,
247
+ onChangeText: (newBranch) => update({ newBranch }),
248
+ disabled: busy
249
+ })]
250
+ }), /* @__PURE__ */ jsxs(View, {
251
+ style: styles$3.field,
252
+ children: [/* @__PURE__ */ jsx(Label, { children: "From" }), /* @__PURE__ */ jsxs(ScrollView, {
253
+ style: styles$3.list,
254
+ nestedScrollEnabled: true,
255
+ children: [/* @__PURE__ */ jsx(ListRow, {
256
+ accessibilityLabel: "From",
257
+ title: /* @__PURE__ */ jsx(Text, {
258
+ style: styles$3.mono,
259
+ children: state.baseRef || "Current branch"
260
+ }),
261
+ disabled: busy,
262
+ onPress: () => setBasesOpen((open) => !open)
263
+ }), basesOpen ? bases.map((base) => /* @__PURE__ */ jsx(ListRow, {
264
+ title: /* @__PURE__ */ jsx(Text, {
265
+ style: styles$3.mono,
266
+ children: base
267
+ }),
268
+ selected: state.baseRef === base,
269
+ disabled: busy,
270
+ disclosure: false,
271
+ onPress: () => {
272
+ update({ baseRef: base });
273
+ setBasesOpen(false);
274
+ }
275
+ }, base)) : null]
276
+ })]
277
+ })] }) : /* @__PURE__ */ jsx(Text, {
278
+ style: styles$3.dim,
279
+ children: "Start in this workspace folder, without creating a worktree."
280
+ })] }),
281
+ /* @__PURE__ */ jsxs(View, {
282
+ style: styles$3.field,
283
+ children: [/* @__PURE__ */ jsx(Label, { children: "Name" }), /* @__PURE__ */ jsx(Input, {
284
+ testID: "new-session-name",
285
+ accessibilityLabel: "Session name",
286
+ placeholder: mode === "plain" ? "Optional" : "Optional, defaults to the branch",
287
+ value: form.name,
288
+ onChangeText: (name) => update({ name }),
289
+ disabled: busy
290
+ })]
291
+ }),
292
+ mode !== "plain" ? /* @__PURE__ */ jsx(Text, {
293
+ style: styles$3.dim,
294
+ children: "Runs in its own worktree, so this checkout stays as it is."
295
+ }) : null,
296
+ error ? /* @__PURE__ */ jsx(Text, {
297
+ testID: "new-session-error",
298
+ style: styles$3.error,
299
+ accessibilityRole: "alert",
300
+ children: error
301
+ }) : null,
302
+ /* @__PURE__ */ jsx(Button, {
303
+ testID: "new-session-create",
304
+ variant: "primary",
305
+ size: "lg",
306
+ loading: busy,
307
+ loadingLabel: mode === "plain" ? "Creating session" : "Creating worktree",
308
+ disabled: loading || problem !== void 0,
309
+ accessibilityHint: problem,
310
+ onPress: () => onCreate(state),
311
+ children: "Create session"
312
+ })
313
+ ]
314
+ })] });
315
+ }
316
+ function errorOf(body, status) {
317
+ return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : `The cockpit answered ${String(status)}.`;
318
+ }
319
+ /** The native new-session sheet when git is installed; mirrors GitNewSessionDialog's calls. */
320
+ function GitNewSessionSheet({ newSession }) {
321
+ const [branches, setBranches] = useState(null);
322
+ const [loadError, setLoadError] = useState("");
323
+ const [error, setError] = useState("");
324
+ const [busy, setBusy] = useState(false);
325
+ const workspaceId = newSession?.workspaceId;
326
+ const request = useEffectEvent((input) => newSession?.requestWithStepUp(input));
327
+ useEffect(() => {
328
+ if (workspaceId === void 0) return;
329
+ let stale = false;
330
+ request(api.workspace(workspaceId).branches.url())?.then(async (response) => {
331
+ const body = await response.json().catch(() => void 0);
332
+ if (stale) return;
333
+ if (!response.ok) setLoadError(errorOf(body, response.status));
334
+ else setBranches(body);
335
+ }).catch(() => {
336
+ if (!stale) setLoadError("The branches could not be loaded.");
337
+ });
338
+ return () => {
339
+ stale = true;
340
+ };
341
+ }, [workspaceId]);
342
+ if (newSession === void 0) return null;
343
+ const submit = async (state) => {
344
+ if (busy || formProblem(state) !== void 0) return;
345
+ setBusy(true);
346
+ setError("");
347
+ const body = createRequest(state);
348
+ let failure;
349
+ try {
350
+ if (body === void 0) failure = await newSession.createPlain(state.name);
351
+ else {
352
+ const response = await newSession.requestWithStepUp(api.workspace(newSession.workspaceId).createSession.url(), {
353
+ method: "POST",
354
+ headers: { "Content-Type": "application/json" },
355
+ body: JSON.stringify(body)
356
+ });
357
+ const answer = await response.json().catch(() => void 0);
358
+ failure = response.ok ? await newSession.openCreated(answer.sessionId) : errorOf(answer, response.status);
359
+ }
360
+ } catch {
361
+ failure = "The cockpit is unreachable.";
362
+ }
363
+ if (failure === void 0) return;
364
+ setError(failure);
365
+ setBusy(false);
366
+ };
367
+ return /* @__PURE__ */ jsx(GitNewSessionSheetView, {
368
+ workspaceRoot: newSession.workspaceRoot,
369
+ branches,
370
+ loadError,
371
+ busy,
372
+ error,
373
+ onCreate: (state) => void submit(state)
374
+ });
375
+ }
376
+ const styles$3 = StyleSheet.create((theme) => {
377
+ const sans = fontOf(theme.fontFamily.sans);
378
+ const mono = fontOf(theme.fontFamily.mono);
379
+ return {
380
+ body: {
381
+ gap: theme.spacing(4),
382
+ uni__dependencies: [0]
383
+ },
384
+ root: {
385
+ fontFamily: mono,
386
+ fontSize: theme.fontSize.sm,
387
+ lineHeight: theme.lineHeight.sm,
388
+ color: theme.colors.dim,
389
+ textAlign: "center",
390
+ uni__dependencies: [0]
391
+ },
392
+ field: {
393
+ gap: theme.spacing(1.5),
394
+ uni__dependencies: [0]
395
+ },
396
+ dim: {
397
+ fontFamily: sans,
398
+ fontSize: theme.fontSize.sm,
399
+ lineHeight: theme.lineHeightRelaxed.sm,
400
+ color: theme.colors.dim,
401
+ uni__dependencies: [0]
402
+ },
403
+ error: {
404
+ fontFamily: sans,
405
+ fontSize: theme.fontSize.sm,
406
+ lineHeight: theme.lineHeightRelaxed.sm,
407
+ color: theme.colors.red,
408
+ uni__dependencies: [0]
409
+ },
410
+ mono: {
411
+ fontFamily: mono,
412
+ fontSize: theme.fontSize.base,
413
+ lineHeight: theme.lineHeight.base,
414
+ color: theme.colors.hi,
415
+ uni__dependencies: [0]
416
+ },
417
+ list: {
418
+ maxHeight: 320,
419
+ borderRadius: theme.borderRadius.lg,
420
+ borderWidth: 1,
421
+ borderColor: theme.colors["border-soft"],
422
+ overflow: "hidden",
423
+ uni__dependencies: [0]
424
+ },
425
+ pad: {
426
+ paddingHorizontal: theme.spacing(4),
427
+ paddingVertical: theme.spacing(2),
428
+ uni__dependencies: [0]
429
+ },
430
+ divider: {
431
+ borderTopWidth: StyleSheet.hairlineWidth,
432
+ borderTopColor: theme.colors["border-soft"],
433
+ uni__dependencies: [0]
434
+ }
435
+ };
436
+ });
437
+ //#endregion
438
+ //#region src/extensions/workspaces/(frontend)/fill/GitNewSession.new-session.native.tsx
439
+ var GitNewSession_new_session_native_default = defineFill({ component: GitNewSessionSheet });
440
+ //#endregion
441
+ //#region src/types/webWorktrees.ts
442
+ /**
443
+ * The cockpit's view of this session's worktrees.
444
+ *
445
+ * DESIGN PATTERNS:
446
+ * - Declarations only. The channel frame type is a constant so the hub and the
447
+ * page cannot drift apart on a string.
448
+ * - A view is the subset of a record the dock needs, not the record. The
449
+ * registry may hold paths and provenance the page has no business rendering.
450
+ *
451
+ * AVOID:
452
+ * - Adding behaviour here. This file is imported by browser code, so it must
453
+ * stay free of node builtins.
454
+ */
455
+ /** The data channel carrying one session's worktrees to the page. */
456
+ const GIT_WORKTREES_TYPE = "git_worktrees";
457
+ //#endregion
458
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/worktreesActivityStore.native.ts
459
+ /**
460
+ * The dock's view of this session's worktrees, and the commands it sends.
461
+ *
462
+ * DESIGN PATTERNS:
463
+ * - Session-scoped store fed by one hub channel. The hub is authoritative; the
464
+ * page never reads a registry and never guesses.
465
+ * - Actions are channel frames, not prompts. The hub half owns the tool's
466
+ * operations, so the panel names the work and the hub performs it; a
467
+ * worktree made from the dock is the same object the tool makes, without
468
+ * spending a turn of the conversation to ask for it.
469
+ * - The sender lives in a store rather than a module variable, because a web
470
+ * plugin keeps no mutable module state and the page may reconnect.
471
+ *
472
+ * AVOID:
473
+ * - Calling the hub package API from here. A dock slot gets no sealed request
474
+ * transport, and raw fetch breaks the remote cockpit.
475
+ * - `sendSessionFrame` for a command. It envelopes the frame as a session
476
+ * command bound for the agent, which never reaches a channel's `receive`.
477
+ */
478
+ /** Presentation state follows hub operation labels, including session startup progress. */
479
+ function worktreesPresentation(state) {
480
+ return {
481
+ working: state.worktrees.filter((item) => !item.orphaned && !item.unowned),
482
+ attention: state.worktrees.filter((item) => item.orphaned || item.unowned),
483
+ starting: state.requestedBranch !== void 0 ? !state.error && !state.worktrees.some((item) => item.branch === state.requestedBranch) : !!state.pending && /^(creating|starting session)/i.test(state.pending),
484
+ closeErrorId: state.errorTarget?.action === "close" ? state.errorTarget.id : void 0
485
+ };
486
+ }
487
+ const worktreeActivity = defineSessionStore({
488
+ worktrees: [],
489
+ pending: void 0,
490
+ error: void 0,
491
+ errorTarget: void 0
492
+ });
493
+ /** Captures the clicked card, so a focus change cannot retarget an open dialog. */
494
+ const worktreeCreateDialog = defineGlobalStore(null);
495
+ /**
496
+ * The page's hub socket sender, held for as long as the plugin runs.
497
+ *
498
+ * Undefined before `startWorktreeRuntime` and after it disposes; a command
499
+ * sent in that window is dropped rather than queued, because a queue would
500
+ * replay a worktree create against a socket that has since changed sessions.
501
+ */
502
+ const worktreeSender = defineGlobalStore(void 0);
503
+ /** Binds the page's hub socket. The plugin host calls this once per page. */
504
+ function startWorktreeRuntime(runtime) {
505
+ const send = (frame) => {
506
+ runtime.sendHubFrame(frame);
507
+ };
508
+ worktreeSender.update(() => send);
509
+ return () => {
510
+ worktreeSender.update((current) => current === send ? void 0 : current);
511
+ worktreeCreateDialog.update(() => null);
512
+ };
513
+ }
514
+ /**
515
+ * Whether this session has worktree work in flight.
516
+ *
517
+ * A standing worktree is state, not work: it sits there until someone closes
518
+ * it, so counting it would leave the session reading as busy forever, with a
519
+ * "background work is still running" notice under a conversation where
520
+ * nothing is running. Only a create or close actually in progress counts.
521
+ *
522
+ * The group stays visible either way. Declaring an active source at all is
523
+ * what keeps it in the dock; this answers how it is drawn, not whether it is
524
+ * there.
525
+ */
526
+ const worktreeActivitySource = {
527
+ subscribe(listener) {
528
+ const subscription = worktreeActivity.store.subscribe(listener);
529
+ return () => subscription.unsubscribe();
530
+ },
531
+ isActive(sessionId) {
532
+ return worktreeActivity.select(worktreeActivity.store.state, sessionId).pending !== void 0;
533
+ }
534
+ };
535
+ function isRecord$2(value) {
536
+ return typeof value === "object" && value !== null && !Array.isArray(value);
537
+ }
538
+ function optionalText$1(value) {
539
+ return typeof value === "string" && value !== "" ? value : void 0;
540
+ }
541
+ function optionalErrorTarget$1(value) {
542
+ if (!isRecord$2(value)) return void 0;
543
+ if (value.action === "create") return value.riftMissing === true ? {
544
+ action: "create",
545
+ riftMissing: true
546
+ } : { action: "create" };
547
+ if (value.action === "close" && typeof value.id === "string" && value.id !== "") return {
548
+ action: "close",
549
+ id: value.id
550
+ };
551
+ }
552
+ /** The plugin's session data channel: 'git_worktrees' payloads into the store. */
553
+ const worktreesChannel = worktreeActivity.channel({
554
+ channel: GIT_WORKTREES_TYPE,
555
+ parse(input) {
556
+ if (!isRecord$2(input) || !Array.isArray(input.worktrees)) return null;
557
+ return {
558
+ worktrees: input.worktrees.filter(isRecord$2),
559
+ pending: optionalText$1(input.pending),
560
+ error: optionalText$1(input.error),
561
+ errorTarget: optionalErrorTarget$1(input.errorTarget)
562
+ };
563
+ },
564
+ reduce(current, next) {
565
+ const requestedBranch = current.requestedBranch;
566
+ return {
567
+ ...next,
568
+ ...requestedBranch && next.pending && !next.error ? { requestedBranch } : {}
569
+ };
570
+ }
571
+ });
572
+ /** Sends one command to the session's worktrees channel. */
573
+ function command(sessionId, payload) {
574
+ worktreeSender.store.state?.({
575
+ type: GIT_WORKTREES_TYPE,
576
+ sessionId,
577
+ payload
578
+ });
579
+ }
580
+ /** Asks the hub for a worktree. The hub answers on the same channel. */
581
+ function requestWorktreeCreate(sessionId, branch, baseRef, reservationId) {
582
+ const trimmedBase = baseRef.trim();
583
+ if (worktreeSender.store.state) worktreeActivity.update(sessionId, (current) => ({
584
+ ...current,
585
+ requestedBranch: branch.trim(),
586
+ pending: "creating",
587
+ error: void 0,
588
+ errorTarget: void 0
589
+ }));
590
+ command(sessionId, {
591
+ action: "create",
592
+ branch: branch.trim(),
593
+ ...trimmedBase === "" ? {} : { baseRef: trimmedBase },
594
+ ...reservationId === void 0 ? {} : { reservationId }
595
+ });
596
+ }
597
+ /** Asks the hub to close one worktree. Refusal on a dirty tree stays the hub's call. */
598
+ function requestWorktreeClose(sessionId, id) {
599
+ command(sessionId, {
600
+ action: "close",
601
+ id
602
+ });
603
+ }
604
+ //#endregion
605
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/WorktreesPanel.native.tsx
606
+ function useWorktrees(sessionId) {
607
+ const subscribe = useCallback((listener) => {
608
+ const sub = worktreeActivity.store.subscribe(listener);
609
+ return () => sub.unsubscribe();
610
+ }, []);
611
+ return useSyncExternalStore(subscribe, () => worktreeActivity.select(worktreeActivity.store.state, sessionId));
612
+ }
613
+ function worktreesTab() {
614
+ return {
615
+ id: "git-worktrees",
616
+ label: "worktrees",
617
+ panel: WorktreesPanel
618
+ };
619
+ }
620
+ function WorktreesHeader({ activity = false, onNew, disabled = false }) {
621
+ return /* @__PURE__ */ jsxs(View, {
622
+ style: styles$2.header,
623
+ children: [/* @__PURE__ */ jsx(Text, {
624
+ style: styles$2.title,
625
+ children: activity ? "Activity" : "Worktrees"
626
+ }), !activity ? /* @__PURE__ */ jsx(Button, {
627
+ testID: "git-worktree-create",
628
+ variant: "ghost",
629
+ style: styles$2.newPill,
630
+ disabled,
631
+ onPress: onNew,
632
+ children: /* @__PURE__ */ jsx(Text, {
633
+ style: styles$2.jade,
634
+ children: "+ New"
635
+ })
636
+ }) : null]
637
+ });
638
+ }
639
+ function WorktreeRow({ item, error, compact = false, onClose, onOpen, disabled = false }) {
640
+ return /* @__PURE__ */ jsxs(View, {
641
+ style: styles$2.worktree,
642
+ children: [
643
+ /* @__PURE__ */ jsxs(View, {
644
+ style: styles$2.line,
645
+ children: [
646
+ /* @__PURE__ */ jsx(Dot, {
647
+ tone: item.orphaned ? "yellow" : item.unowned ? "blue" : "green",
648
+ size: "md"
649
+ }),
650
+ /* @__PURE__ */ jsxs(Pressable, {
651
+ style: styles$2.grow,
652
+ accessibilityRole: onOpen ? "button" : void 0,
653
+ accessibilityLabel: onOpen ? `Open session for ${item.branch}` : void 0,
654
+ onPress: onOpen,
655
+ disabled: !onOpen,
656
+ children: [/* @__PURE__ */ jsx(Text, {
657
+ numberOfLines: 1,
658
+ ellipsizeMode: "middle",
659
+ style: styles$2.branch,
660
+ children: compact && item.branch.length > 36 ? `${item.branch.slice(0, 19)}…${item.branch.slice(-14)}` : item.branch
661
+ }), !compact ? /* @__PURE__ */ jsx(Text, {
662
+ style: styles$2.dim,
663
+ children: item.orphaned ? "Its folder is missing" : item.unowned ? "Its session has ended" : "Session running"
664
+ }) : null]
665
+ }),
666
+ item.orphaned ? /* @__PURE__ */ jsx(Badge, {
667
+ tone: "yellow",
668
+ style: styles$2.caution,
669
+ children: "Orphaned"
670
+ }) : null,
671
+ item.unowned ? /* @__PURE__ */ jsx(Badge, {
672
+ tone: "blue",
673
+ style: styles$2.info,
674
+ children: "Unowned"
675
+ }) : null,
676
+ !compact ? /* @__PURE__ */ jsx(Button, {
677
+ variant: "ghost",
678
+ style: styles$2.more,
679
+ accessibilityLabel: `Close ${item.branch}`,
680
+ testID: `git-worktree-close-${item.id}`,
681
+ onPress: onClose,
682
+ disabled,
683
+ children: "⋮"
684
+ }) : /* @__PURE__ */ jsx(ChevronRightIcon, {
685
+ size: 16,
686
+ color: "dim"
687
+ })
688
+ ]
689
+ }),
690
+ !compact ? /* @__PURE__ */ jsx(Text, {
691
+ numberOfLines: 1,
692
+ ellipsizeMode: "middle",
693
+ style: styles$2.path,
694
+ children: item.path
695
+ }) : null,
696
+ error ? /* @__PURE__ */ jsx(Text, {
697
+ accessibilityRole: "alert",
698
+ style: styles$2.error,
699
+ children: error
700
+ }) : null
701
+ ]
702
+ });
703
+ }
704
+ function WorktreeStarting({ branch }) {
705
+ return /* @__PURE__ */ jsxs(View, {
706
+ style: styles$2.starting,
707
+ children: [/* @__PURE__ */ jsx(Spinner, { label: "Starting session..." }), /* @__PURE__ */ jsxs(View, {
708
+ style: styles$2.grow,
709
+ children: [branch ? /* @__PURE__ */ jsx(Text, {
710
+ numberOfLines: 1,
711
+ ellipsizeMode: "middle",
712
+ style: styles$2.branch,
713
+ children: branch
714
+ }) : null, /* @__PURE__ */ jsx(Text, {
715
+ style: styles$2.dim,
716
+ children: "Starting session..."
717
+ })]
718
+ })]
719
+ });
720
+ }
721
+ function WorktreeGroup({ title, count }) {
722
+ return /* @__PURE__ */ jsxs(View, {
723
+ style: styles$2.group,
724
+ children: [/* @__PURE__ */ jsx(Text, {
725
+ style: styles$2.heading,
726
+ children: title
727
+ }), /* @__PURE__ */ jsx(Text, {
728
+ style: styles$2.dim,
729
+ children: count
730
+ })]
731
+ });
732
+ }
733
+ function WorktreeForm({ close = false, branch = "", baseRef = "", onBranch, onBaseRef, onConfirm, onCancel, disabled = false, framed = true }) {
734
+ return /* @__PURE__ */ jsxs(View, {
735
+ style: [styles$2.sheet, !framed && styles$2.unframed],
736
+ children: [
737
+ framed ? /* @__PURE__ */ jsx(View, { style: styles$2.handle }) : null,
738
+ /* @__PURE__ */ jsx(Text, {
739
+ style: styles$2.sheetTitle,
740
+ children: close ? "Close this worktree?" : "New worktree"
741
+ }),
742
+ close ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
743
+ style: styles$2.branch,
744
+ children: branch
745
+ }), /* @__PURE__ */ jsx(Text, {
746
+ style: styles$2.body,
747
+ children: "Removes the checkout and stops its session."
748
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
749
+ /* @__PURE__ */ jsx(Text, {
750
+ style: styles$2.body,
751
+ children: "A separate checkout on its own branch, with its own session. This session stays where it is."
752
+ }),
753
+ /* @__PURE__ */ jsx(Text, {
754
+ style: styles$2.label,
755
+ children: "Branch"
756
+ }),
757
+ /* @__PURE__ */ jsx(Input, {
758
+ accessibilityLabel: "Branch",
759
+ value: branch,
760
+ onChangeText: onBranch,
761
+ style: styles$2.monoInput
762
+ }),
763
+ /* @__PURE__ */ jsx(Text, {
764
+ style: styles$2.label,
765
+ children: "Start from"
766
+ }),
767
+ /* @__PURE__ */ jsx(Input, {
768
+ accessibilityLabel: "Start from",
769
+ value: baseRef,
770
+ onChangeText: onBaseRef,
771
+ placeholder: "Current branch"
772
+ })
773
+ ] }),
774
+ /* @__PURE__ */ jsx(Button, {
775
+ onPress: onConfirm,
776
+ disabled: disabled || !close && branch.trim() === "",
777
+ variant: close ? "danger" : "primary",
778
+ style: [styles$2.fullButton, !close && styles$2.createButton],
779
+ children: close ? "Close worktree" : /* @__PURE__ */ jsx(Text, {
780
+ style: styles$2.createLabel,
781
+ children: "Create worktree"
782
+ })
783
+ }),
784
+ /* @__PURE__ */ jsx(Button, {
785
+ variant: "outline",
786
+ style: styles$2.fullButton,
787
+ onPress: onCancel,
788
+ children: "Cancel"
789
+ })
790
+ ]
791
+ });
792
+ }
793
+ function WorktreesView({ state, disabled = false, onNew, onClose, onOpen }) {
794
+ const { working, attention, starting, closeErrorId } = worktreesPresentation(state);
795
+ const row = (item) => /* @__PURE__ */ jsx(WorktreeRow, {
796
+ item,
797
+ disabled: disabled || !!state.pending,
798
+ onClose: () => onClose?.(item),
799
+ onOpen: item.sessionId && onOpen ? () => onOpen(item.sessionId) : void 0,
800
+ error: state.errorTarget?.action === "close" && state.errorTarget.id === item.id ? state.error : void 0
801
+ }, item.id);
802
+ return /* @__PURE__ */ jsxs(View, {
803
+ style: styles$2.screen,
804
+ children: [
805
+ /* @__PURE__ */ jsx(WorktreesHeader, {
806
+ disabled: disabled || !!state.pending,
807
+ onNew
808
+ }),
809
+ state.worktrees.length || starting ? /* @__PURE__ */ jsxs(View, {
810
+ style: styles$2.content,
811
+ children: [
812
+ /* @__PURE__ */ jsx(WorktreeGroup, {
813
+ title: "Working",
814
+ count: `${working.length} open${starting ? " · 1 starting" : ""}`
815
+ }),
816
+ working.map(row),
817
+ starting ? /* @__PURE__ */ jsx(WorktreeStarting, { branch: state.requestedBranch }) : null,
818
+ attention.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
819
+ /* @__PURE__ */ jsx(WorktreeGroup, {
820
+ title: "Needs a look",
821
+ count: `${attention.length} open`
822
+ }),
823
+ /* @__PURE__ */ jsxs(Text, {
824
+ style: styles$2.dim,
825
+ children: [
826
+ attention.length === 2 ? "Two worktrees" : attention.length === 1 ? "One worktree" : `${attention.length} worktrees`,
827
+ " ",
828
+ "would like your eye, whenever you are ready."
829
+ ]
830
+ }),
831
+ attention.map(row)
832
+ ] }) : null
833
+ ]
834
+ }) : /* @__PURE__ */ jsxs(View, {
835
+ style: styles$2.empty,
836
+ children: [/* @__PURE__ */ jsx(Text, {
837
+ style: styles$2.heading,
838
+ children: "No worktrees yet"
839
+ }), /* @__PURE__ */ jsx(Text, {
840
+ style: styles$2.emptyCopy,
841
+ children: "Create a worktree to run a separate session on its own branch."
842
+ })]
843
+ }),
844
+ state.pending && !starting ? /* @__PURE__ */ jsx(Text, {
845
+ style: styles$2.dim,
846
+ children: state.pending
847
+ }) : null,
848
+ state.error && (!closeErrorId || !state.worktrees.some((item) => item.id === closeErrorId)) ? /* @__PURE__ */ jsx(Text, {
849
+ accessibilityRole: "alert",
850
+ style: styles$2.error,
851
+ children: state.error
852
+ }) : null
853
+ ]
854
+ });
855
+ }
856
+ function WorktreesPanel({ sessionId, sessionReservationId, openSession }) {
857
+ const state = useWorktrees(sessionId);
858
+ const [creating, setCreating] = useState(false);
859
+ const [branch, setBranch] = useState("");
860
+ const [baseRef, setBaseRef] = useState("");
861
+ const [closing, setClosing] = useState();
862
+ const busy = sessionId === null || state.pending !== void 0;
863
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
864
+ /* @__PURE__ */ jsx(ScrollView, {
865
+ contentContainerStyle: styles$2.scroll,
866
+ children: /* @__PURE__ */ jsx(WorktreesView, {
867
+ state,
868
+ disabled: sessionId === null,
869
+ onNew: () => setCreating(true),
870
+ onClose: setClosing,
871
+ onOpen: openSession
872
+ })
873
+ }),
874
+ /* @__PURE__ */ jsx(Sheet, {
875
+ open: creating,
876
+ onOpenChange: setCreating,
877
+ children: /* @__PURE__ */ jsx(SheetContent, { children: /* @__PURE__ */ jsx(WorktreeForm, {
878
+ framed: false,
879
+ branch,
880
+ baseRef,
881
+ onBranch: setBranch,
882
+ onBaseRef: setBaseRef,
883
+ disabled: busy,
884
+ onCancel: () => setCreating(false),
885
+ onConfirm: () => {
886
+ if (sessionId === null) return;
887
+ requestWorktreeCreate(sessionId, branch, baseRef, sessionReservationId);
888
+ setCreating(false);
889
+ if (sessionReservationId === void 0) {
890
+ setBranch("");
891
+ setBaseRef("");
892
+ }
893
+ }
894
+ }) })
895
+ }),
896
+ /* @__PURE__ */ jsx(Sheet, {
897
+ open: !!closing,
898
+ onOpenChange: (open) => {
899
+ if (!open) setClosing(void 0);
900
+ },
901
+ children: /* @__PURE__ */ jsx(SheetContent, { children: /* @__PURE__ */ jsx(WorktreeForm, {
902
+ framed: false,
903
+ close: true,
904
+ branch: closing?.branch,
905
+ disabled: busy,
906
+ onCancel: () => setClosing(void 0),
907
+ onConfirm: () => {
908
+ if (sessionId !== null && closing) requestWorktreeClose(sessionId, closing.id);
909
+ setClosing(void 0);
910
+ }
911
+ }) })
912
+ })
913
+ ] });
914
+ }
915
+ const styles$2 = StyleSheet.create((theme) => ({
916
+ screen: {
917
+ flex: 1,
918
+ backgroundColor: theme.colors.rail,
919
+ uni__dependencies: [0]
920
+ },
921
+ scroll: { flexGrow: 1 },
922
+ header: {
923
+ flexDirection: "row",
924
+ alignItems: "center",
925
+ justifyContent: "space-between",
926
+ padding: theme.spacing(5),
927
+ minHeight: 88,
928
+ uni__dependencies: [0]
929
+ },
930
+ title: {
931
+ fontFamily: fontOf(theme.fontFamily.sans),
932
+ fontSize: theme.fontSize["2xl"],
933
+ fontWeight: "600",
934
+ color: theme.colors.hi,
935
+ uni__dependencies: [0]
936
+ },
937
+ newPill: {
938
+ backgroundColor: theme.colors["tint-green"],
939
+ borderRadius: theme.borderRadius.full,
940
+ minHeight: 44,
941
+ paddingHorizontal: theme.spacing(4),
942
+ uni__dependencies: [0]
943
+ },
944
+ content: {
945
+ paddingHorizontal: theme.spacing(5),
946
+ gap: theme.spacing(2),
947
+ uni__dependencies: [0]
948
+ },
949
+ group: {
950
+ flexDirection: "row",
951
+ alignItems: "center",
952
+ gap: theme.spacing(3),
953
+ paddingTop: theme.spacing(4),
954
+ paddingBottom: theme.spacing(2),
955
+ borderBottomWidth: StyleSheet.hairlineWidth,
956
+ borderColor: theme.colors["border-soft"],
957
+ uni__dependencies: [0]
958
+ },
959
+ heading: {
960
+ fontFamily: fontOf(theme.fontFamily.sans),
961
+ fontSize: theme.fontSize.lg,
962
+ lineHeight: theme.lineHeight.lg,
963
+ fontWeight: "600",
964
+ color: theme.colors.hi,
965
+ uni__dependencies: [0]
966
+ },
967
+ dim: {
968
+ fontFamily: fontOf(theme.fontFamily.sans),
969
+ fontSize: theme.fontSize.sm,
970
+ lineHeight: theme.lineHeight.sm,
971
+ color: theme.colors.dim,
972
+ uni__dependencies: [0]
973
+ },
974
+ body: {
975
+ fontFamily: fontOf(theme.fontFamily.sans),
976
+ fontSize: theme.fontSize.base,
977
+ lineHeight: theme.lineHeight.lg,
978
+ color: theme.colors.dim,
979
+ uni__dependencies: [0]
980
+ },
981
+ label: {
982
+ fontFamily: fontOf(theme.fontFamily.sans),
983
+ fontSize: theme.fontSize.base,
984
+ fontWeight: "500",
985
+ color: theme.colors.text,
986
+ marginTop: theme.spacing(2),
987
+ uni__dependencies: [0]
988
+ },
989
+ branch: {
990
+ fontFamily: fontOf(theme.fontFamily.mono),
991
+ fontSize: theme.fontSize.base,
992
+ lineHeight: theme.lineHeight.lg,
993
+ color: theme.colors.hi,
994
+ uni__dependencies: [0]
995
+ },
996
+ path: {
997
+ fontFamily: fontOf(theme.fontFamily.mono),
998
+ fontSize: theme.fontSize.sm,
999
+ lineHeight: theme.lineHeight.sm,
1000
+ color: theme.colors.dim,
1001
+ marginLeft: theme.spacing(4.5),
1002
+ uni__dependencies: [0]
1003
+ },
1004
+ worktree: {
1005
+ gap: theme.spacing(1),
1006
+ paddingVertical: theme.spacing(2),
1007
+ borderBottomWidth: StyleSheet.hairlineWidth,
1008
+ borderColor: theme.colors["border-soft"],
1009
+ uni__dependencies: [0]
1010
+ },
1011
+ line: {
1012
+ flexDirection: "row",
1013
+ alignItems: "center",
1014
+ gap: theme.spacing(2),
1015
+ minHeight: 44,
1016
+ uni__dependencies: [0]
1017
+ },
1018
+ grow: {
1019
+ flex: 1,
1020
+ minWidth: 0,
1021
+ minHeight: 44,
1022
+ justifyContent: "center",
1023
+ gap: theme.spacing(1),
1024
+ uni__dependencies: [0]
1025
+ },
1026
+ more: {
1027
+ width: 44,
1028
+ height: 44,
1029
+ padding: 0
1030
+ },
1031
+ caution: {
1032
+ backgroundColor: theme.colors["tint-yellow"],
1033
+ uni__dependencies: [0]
1034
+ },
1035
+ info: {
1036
+ backgroundColor: theme.colors["tint-blue"],
1037
+ uni__dependencies: [0]
1038
+ },
1039
+ starting: {
1040
+ flexDirection: "row",
1041
+ alignItems: "center",
1042
+ gap: theme.spacing(2),
1043
+ minHeight: 52,
1044
+ uni__dependencies: [0]
1045
+ },
1046
+ jade: {
1047
+ fontFamily: fontOf(theme.fontFamily.sans),
1048
+ fontSize: theme.fontSize.base,
1049
+ lineHeight: theme.lineHeight.base,
1050
+ color: theme.colors.green,
1051
+ fontWeight: "500",
1052
+ uni__dependencies: [0]
1053
+ },
1054
+ error: {
1055
+ fontFamily: fontOf(theme.fontFamily.sans),
1056
+ fontSize: theme.fontSize.sm,
1057
+ lineHeight: theme.lineHeight.sm,
1058
+ color: theme.colors.red,
1059
+ uni__dependencies: [0]
1060
+ },
1061
+ unframed: { borderTopWidth: 0 },
1062
+ sheet: {
1063
+ backgroundColor: theme.colors.bg,
1064
+ borderTopWidth: 1,
1065
+ borderColor: theme.colors.border,
1066
+ borderTopLeftRadius: theme.borderRadius.lg,
1067
+ borderTopRightRadius: theme.borderRadius.lg,
1068
+ padding: theme.spacing(5),
1069
+ gap: theme.spacing(3),
1070
+ uni__dependencies: [0]
1071
+ },
1072
+ handle: {
1073
+ alignSelf: "center",
1074
+ width: 40,
1075
+ height: 4,
1076
+ borderRadius: theme.borderRadius.full,
1077
+ backgroundColor: theme.colors.border,
1078
+ marginBottom: theme.spacing(2),
1079
+ uni__dependencies: [0]
1080
+ },
1081
+ sheetTitle: {
1082
+ fontFamily: fontOf(theme.fontFamily.sans),
1083
+ fontSize: theme.fontSize.xl,
1084
+ fontWeight: "600",
1085
+ color: theme.colors.hi,
1086
+ uni__dependencies: [0]
1087
+ },
1088
+ monoInput: {
1089
+ fontFamily: fontOf(theme.fontFamily.mono),
1090
+ uni__dependencies: [0]
1091
+ },
1092
+ fullButton: {
1093
+ height: 44,
1094
+ alignSelf: "stretch"
1095
+ },
1096
+ createButton: {
1097
+ backgroundColor: theme.colors.green,
1098
+ uni__dependencies: [0]
1099
+ },
1100
+ createLabel: {
1101
+ fontFamily: fontOf(theme.fontFamily.sans),
1102
+ fontSize: theme.fontSize.base,
1103
+ fontWeight: "600",
1104
+ color: theme.colors.bg,
1105
+ uni__dependencies: [0]
1106
+ },
1107
+ empty: {
1108
+ flex: 1,
1109
+ alignItems: "center",
1110
+ justifyContent: "center",
1111
+ padding: theme.spacing(8),
1112
+ gap: theme.spacing(3),
1113
+ uni__dependencies: [0]
1114
+ },
1115
+ emptyCopy: {
1116
+ fontFamily: fontOf(theme.fontFamily.sans),
1117
+ fontSize: theme.fontSize.base,
1118
+ lineHeight: theme.lineHeight.lg,
1119
+ color: theme.colors.dim,
1120
+ textAlign: "center",
1121
+ uni__dependencies: [0]
1122
+ }
1123
+ }));
1124
+ //#endregion
1125
+ //#region src/types/gitReview.ts
1126
+ /**
1127
+ * The cockpit's view of what this session changed against its base: the counts
1128
+ * the '# diff' group shows, what the review tab lists and draws, and the manual
1129
+ * sync that sits beside them.
1130
+ *
1131
+ * DESIGN PATTERNS:
1132
+ * - Declarations only, shared by the hub and the page.
1133
+ * - Its own channel, apart from worktrees. Worktrees are repository
1134
+ * management; the diff is this session's code change.
1135
+ * - A diff row carries one line number whose file follows from its marker: the
1136
+ * new file's for context and additions, the old file's for removals. It is
1137
+ * the same shape the shared DiffView draws.
1138
+ *
1139
+ * AVOID:
1140
+ * - Behaviour or node builtins. Browser code imports this file.
1141
+ */
1142
+ /** The session channel carrying this checkout's changes and the sync around them. */
1143
+ const GIT_CHANGES_TYPE = "git_changes";
1144
+ //#endregion
1145
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitChangesStore.native.ts
1146
+ /**
1147
+ * This session's code change against its base, and the manual sync beside it.
1148
+ *
1149
+ * DESIGN PATTERNS:
1150
+ * - Its own session store and hub channel, apart from worktrees. Worktrees are
1151
+ * repository management; this is the session's diff.
1152
+ * - The hub is authoritative: it computes the counts, runs pull, push and
1153
+ * rebase one at a time, and reports pending, errors and a paused rebase here.
1154
+ * - The sender lives in a store rather than a module variable, because a web
1155
+ * plugin keeps no mutable module state and the page may reconnect.
1156
+ *
1157
+ * AVOID:
1158
+ * - `sendSessionFrame` for a sync command. That frame is bound for the agent
1159
+ * and never reaches a channel's `receive`.
1160
+ */
1161
+ const gitChanges = defineSessionStore({
1162
+ changes: void 0,
1163
+ pending: void 0,
1164
+ error: void 0,
1165
+ errorTarget: void 0
1166
+ });
1167
+ /** The page's hub socket sender; undefined before start and after dispose, when a command is dropped. */
1168
+ const changesSender = defineGlobalStore(void 0);
1169
+ /** Binds the page's hub socket for sync commands. The plugin host calls this once per page. */
1170
+ function startGitChangesRuntime(runtime) {
1171
+ const send = (frame) => {
1172
+ runtime.sendHubFrame(frame);
1173
+ };
1174
+ changesSender.update(() => send);
1175
+ return () => {
1176
+ changesSender.update((current) => current === send ? void 0 : current);
1177
+ };
1178
+ }
1179
+ /** Whether a sync command is running for this session. */
1180
+ const gitChangesActivitySource = {
1181
+ subscribe(listener) {
1182
+ const subscription = gitChanges.store.subscribe(listener);
1183
+ return () => subscription.unsubscribe();
1184
+ },
1185
+ isActive(sessionId) {
1186
+ return gitChanges.select(gitChanges.store.state, sessionId).pending !== void 0;
1187
+ }
1188
+ };
1189
+ function isRecord$1(value) {
1190
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1191
+ }
1192
+ function optionalText(value) {
1193
+ return typeof value === "string" && value !== "" ? value : void 0;
1194
+ }
1195
+ function count(value) {
1196
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
1197
+ }
1198
+ /** The page never trusts a frame's shape: a malformed field drops, the rest still renders. */
1199
+ const MAX_CONFLICTS = 100;
1200
+ function optionalChanges(value) {
1201
+ if (!isRecord$1(value)) return void 0;
1202
+ const added = count(value.added);
1203
+ const removed = count(value.removed);
1204
+ const files = count(value.files);
1205
+ if (added === void 0 || removed === void 0 || files === void 0) return void 0;
1206
+ const branch = optionalText(value.branch);
1207
+ const base = optionalText(value.base);
1208
+ const upstream = isRecord$1(value.upstream) ? value.upstream : void 0;
1209
+ const ahead = count(upstream?.ahead);
1210
+ const behind = count(upstream?.behind);
1211
+ const upstreamRef = optionalText(upstream?.ref);
1212
+ const conflicts = isRecord$1(value.rebase) && Array.isArray(value.rebase.conflicts) ? value.rebase.conflicts.filter((file) => typeof file === "string").slice(0, MAX_CONFLICTS) : void 0;
1213
+ return {
1214
+ added,
1215
+ removed,
1216
+ files,
1217
+ ...branch === void 0 ? {} : { branch },
1218
+ ...base === void 0 ? {} : { base },
1219
+ ...value.truncated === true ? { truncated: true } : {},
1220
+ ...upstreamRef === void 0 || ahead === void 0 || behind === void 0 ? {} : { upstream: {
1221
+ ref: upstreamRef,
1222
+ ahead,
1223
+ behind
1224
+ } },
1225
+ ...conflicts === void 0 ? {} : { rebase: { conflicts } }
1226
+ };
1227
+ }
1228
+ function optionalErrorTarget(value) {
1229
+ if (!isRecord$1(value)) return void 0;
1230
+ if (value.action === "pull" || value.action === "rebase" || value.action === "abort-rebase") return { action: value.action };
1231
+ if (value.action === "push") return value.forceRequired === true ? {
1232
+ action: "push",
1233
+ forceRequired: true
1234
+ } : { action: "push" };
1235
+ }
1236
+ /** The session data channel: 'git_changes' payloads into the store. */
1237
+ const gitChangesChannel = gitChanges.channel({
1238
+ channel: GIT_CHANGES_TYPE,
1239
+ parse(input) {
1240
+ if (!isRecord$1(input)) return null;
1241
+ return {
1242
+ changes: optionalChanges(input.changes),
1243
+ pending: optionalText(input.pending),
1244
+ error: optionalText(input.error),
1245
+ errorTarget: optionalErrorTarget(input.errorTarget)
1246
+ };
1247
+ },
1248
+ reduce(_current, next) {
1249
+ return next;
1250
+ }
1251
+ });
1252
+ /**
1253
+ * Asks the hub to pull, push, rebase or abort a paused rebase. `force` is only
1254
+ * ever sent after the reader confirmed a lease force for a rejected push.
1255
+ */
1256
+ function requestGitSync(sessionId, action, force = false) {
1257
+ const payload = action === "push" && force ? {
1258
+ action: "push",
1259
+ force: true
1260
+ } : { action };
1261
+ changesSender.store.state?.({
1262
+ type: GIT_CHANGES_TYPE,
1263
+ sessionId,
1264
+ payload
1265
+ });
1266
+ }
1267
+ //#endregion
1268
+ //#region src/extensions/workspaces/sessions/(frontend)/activity-group/git.native.ts
1269
+ var git_native_default = defineActivityGroup({
1270
+ keys: "g w",
1271
+ icon: () => createElement(BranchIcon),
1272
+ activeSource: {
1273
+ subscribe(listener) {
1274
+ const stopWorktrees = worktreeActivitySource.subscribe(listener);
1275
+ const stopChanges = gitChangesActivitySource.subscribe(listener);
1276
+ return () => {
1277
+ stopWorktrees();
1278
+ stopChanges();
1279
+ };
1280
+ },
1281
+ isActive(sessionId) {
1282
+ return worktreeActivitySource.isActive(sessionId) || gitChangesActivitySource.isActive(sessionId);
1283
+ }
1284
+ },
1285
+ marksBackgroundWork: false,
1286
+ transientTab: worktreesTab,
1287
+ order: 30
1288
+ });
1289
+ //#endregion
1290
+ //#region src/extensions/workspaces/sessions/(frontend)/channel/git-changes.native.ts
1291
+ var git_changes_native_default = defineChannelFile(gitChangesChannel);
1292
+ //#endregion
1293
+ //#region src/extensions/workspaces/sessions/(frontend)/channel/git-worktrees.native.ts
1294
+ var git_worktrees_native_default = defineChannelFile(worktreesChannel);
1295
+ //#endregion
1296
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewApi.native.ts
1297
+ /**
1298
+ * The review tab's half of the session API: the changed-file list and one
1299
+ * file's hunks.
1300
+ *
1301
+ * The generated client owns the URL and the sealed transport, so nothing here
1302
+ * spells a route or reaches for `fetch`. What travels is file contents, which a
1303
+ * tunnel's relay must never see in the clear.
1304
+ */
1305
+ const UNREACHABLE = "The session is unreachable.";
1306
+ function isRecord(value) {
1307
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1308
+ }
1309
+ /** The message a reader is shown: the route's own words, or why there were none. */
1310
+ function messageOf(result) {
1311
+ if (result.status === 0) return UNREACHABLE;
1312
+ return result.error === "" ? `The session answered ${String(result.status)}.` : result.error;
1313
+ }
1314
+ async function fetchReviewSummary(sessionId, signal) {
1315
+ const result = await api.session(sessionId).review(signal === void 0 ? {} : { signal });
1316
+ if (!result.ok) return {
1317
+ ok: false,
1318
+ error: messageOf(result)
1319
+ };
1320
+ if (!isRecord(result.data) || !Array.isArray(result.data.files)) return {
1321
+ ok: false,
1322
+ error: "The session answered with no file list."
1323
+ };
1324
+ return {
1325
+ ok: true,
1326
+ summary: result.data
1327
+ };
1328
+ }
1329
+ async function fetchReviewFile(sessionId, path, signal) {
1330
+ const result = await api.session(sessionId).reviewFile({
1331
+ query: { path },
1332
+ ...signal === void 0 ? {} : { signal }
1333
+ });
1334
+ if (!result.ok) return {
1335
+ ok: false,
1336
+ error: messageOf(result)
1337
+ };
1338
+ if (!isRecord(result.data) || !Array.isArray(result.data.hunks)) return {
1339
+ ok: false,
1340
+ error: "The session answered with no diff."
1341
+ };
1342
+ return {
1343
+ ok: true,
1344
+ diff: result.data
1345
+ };
1346
+ }
1347
+ //#endregion
1348
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewSelection.ts
1349
+ /** A different side starts a fresh selection. The second same-side tap completes it. */
1350
+ function selectReviewLine(anchor, row) {
1351
+ const side = row.marker === "-" ? "old" : "new";
1352
+ if (!anchor || anchor.side !== side) return {
1353
+ range: {
1354
+ side,
1355
+ startLine: row.line,
1356
+ endLine: row.line
1357
+ },
1358
+ complete: false
1359
+ };
1360
+ return {
1361
+ range: {
1362
+ side,
1363
+ startLine: Math.min(anchor.startLine, row.line),
1364
+ endLine: Math.max(anchor.startLine, row.line)
1365
+ },
1366
+ complete: true
1367
+ };
1368
+ }
1369
+ function reviewRangeRows(rows, range) {
1370
+ return rows.filter((row) => (row.marker === "-" ? "old" : "new") === range.side && row.line >= range.startLine && row.line <= range.endLine);
1371
+ }
1372
+ //#endregion
1373
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewStore.native.ts
1374
+ const gitReview = defineSessionStore({ comments: [] });
1375
+ function addReviewComment(sessionId, comment) {
1376
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1377
+ gitReview.update(sessionId, (current) => ({ comments: [...current.comments, {
1378
+ ...comment,
1379
+ id
1380
+ }] }));
1381
+ }
1382
+ function removeReviewComment(sessionId, id) {
1383
+ gitReview.update(sessionId, (current) => ({ comments: current.comments.filter((comment) => comment.id !== id) }));
1384
+ }
1385
+ function clearReviewComments(sessionId) {
1386
+ gitReview.update(sessionId, () => ({ comments: [] }));
1387
+ }
1388
+ //#endregion
1389
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewView.ts
1390
+ /**
1391
+ * Pure wording for what the review tab sends the agent, and the one send.
1392
+ *
1393
+ * Both messages go as `follow_up`: the host starts a turn when idle, or after
1394
+ * the current one, without treating a review send as an explicit Queue action.
1395
+ * A `prompt` frame instead fails while a turn runs.
1396
+ */
1397
+ /** The opening line of a review, naming what was reviewed. */
1398
+ function reviewHeading(count, branch, base) {
1399
+ return `I reviewed ${branch === void 0 ? "this checkout" : `branch ${branch}`} (${base === void 0 ? "its uncommitted changes" : `its changes against ${base}`}) and left ${count === 1 ? "one review comment" : `${String(count)} review comments`}. ${count === 1 ? "Please address it." : "Please address them all."}`;
1400
+ }
1401
+ /** What "ask agent to resolve" sends: the files, and the steps that finish a paused rebase. */
1402
+ function conflictPrompt(branch, conflicts) {
1403
+ const files = conflicts.length === 0 ? "- (git reports no unmerged paths yet)" : conflicts.map((file) => `- ${file}`).join("\n");
1404
+ return [
1405
+ `A rebase of ${branch ?? "this checkout"} stopped on conflicts in:`,
1406
+ "",
1407
+ files,
1408
+ "",
1409
+ "Please resolve them:",
1410
+ "1. Fix the conflict markers in each file, keeping the intent of both sides.",
1411
+ "2. `git add` each resolved file.",
1412
+ "3. Run `GIT_EDITOR=true git rebase --continue`, and repeat if it stops again.",
1413
+ "",
1414
+ "Do not push. I will push from the cockpit once the rebase is done."
1415
+ ].join("\n");
1416
+ }
1417
+ /**
1418
+ * The one sync the bar fills in: pull while the branch is behind (a push would
1419
+ * be rejected), push while it is only ahead or has no upstream yet. Nothing
1420
+ * while a rebase is paused, detached, or already level.
1421
+ */
1422
+ function suggestedSyncAction(changes) {
1423
+ if (changes?.branch === void 0 || changes.rebase !== void 0) return void 0;
1424
+ if (changes.upstream === void 0) return "push";
1425
+ if (changes.upstream.behind > 0) return "pull";
1426
+ return changes.upstream.ahead > 0 ? "push" : void 0;
1427
+ }
1428
+ /** "2 to push, 1 new on origin/feat/git", or "Up to date with origin/feat/git". */
1429
+ function upstreamSummary(upstream) {
1430
+ if (upstream === void 0) return "No upstream yet";
1431
+ const parts = [...upstream.ahead > 0 ? [`${String(upstream.ahead)} to push`] : [], ...upstream.behind > 0 ? [`${String(upstream.behind)} new on ${upstream.ref}`] : []];
1432
+ return parts.length === 0 ? `Up to date with ${upstream.ref}` : parts.join(", ");
1433
+ }
1434
+ /**
1435
+ * Hands one deferred follow-up to the session. Returns the reason it failed,
1436
+ * or undefined once sent. The sender throws when the session is disconnected.
1437
+ */
1438
+ function deliverToAgent(send, sessionId, message) {
1439
+ try {
1440
+ send(sessionId, {
1441
+ type: "follow_up",
1442
+ message
1443
+ });
1444
+ return;
1445
+ } catch (error) {
1446
+ return error instanceof Error ? error.message : "The session did not take the message.";
1447
+ }
1448
+ }
1449
+ //#endregion
1450
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/GitReviewView.native.tsx
1451
+ function Action({ children, variant = "outline", disabled = false, onPress }) {
1452
+ const { theme } = useUnistyles();
1453
+ return /* @__PURE__ */ jsx(Button, {
1454
+ onPress,
1455
+ variant,
1456
+ disabled,
1457
+ style: [styles$1.action, variant === "primary" && styles$1.primary],
1458
+ children: variant === "primary" ? /* @__PURE__ */ jsx(Text, {
1459
+ style: [styles$1.primaryLabel, { color: theme.scheme === "dark" ? theme.colors.rail : theme.colors["on-selected"] }],
1460
+ children
1461
+ }) : children
1462
+ });
1463
+ }
1464
+ function Footer({ count, discard = false, confirmation = false, onReview, onSend, onDiscard }) {
1465
+ return /* @__PURE__ */ jsxs(View, {
1466
+ style: styles$1.footer,
1467
+ children: [discard ? /* @__PURE__ */ jsx(Action, {
1468
+ variant: "ghost",
1469
+ onPress: onDiscard,
1470
+ children: "Discard"
1471
+ }) : /* @__PURE__ */ jsxs(Pressable, {
1472
+ accessibilityRole: "button",
1473
+ accessibilityLabel: "Review queued comments",
1474
+ onPress: onReview,
1475
+ style: [styles$1.inline, styles$1.action],
1476
+ children: [/* @__PURE__ */ jsx(MessageIcon, {
1477
+ size: 16,
1478
+ color: "blue"
1479
+ }), /* @__PURE__ */ jsxs(Text, {
1480
+ style: styles$1.dim,
1481
+ children: [
1482
+ count,
1483
+ " ",
1484
+ count === 1 ? "comment" : "comments",
1485
+ " queued"
1486
+ ]
1487
+ })]
1488
+ }), /* @__PURE__ */ jsx(Action, {
1489
+ variant: confirmation ? "outline" : "primary",
1490
+ disabled: count === 0,
1491
+ onPress: onSend,
1492
+ children: "Send review"
1493
+ })]
1494
+ });
1495
+ }
1496
+ function SyncCard({ changes, busy = false, onSync }) {
1497
+ const suggested = suggestedSyncAction(changes);
1498
+ return /* @__PURE__ */ jsxs(View, {
1499
+ style: styles$1.card,
1500
+ children: [
1501
+ /* @__PURE__ */ jsxs(View, {
1502
+ style: styles$1.inline,
1503
+ children: [
1504
+ /* @__PURE__ */ jsx(BranchIcon, {
1505
+ size: 18,
1506
+ color: "dim"
1507
+ }),
1508
+ /* @__PURE__ */ jsx(Text, {
1509
+ style: styles$1.branch,
1510
+ children: changes?.branch ?? "Detached HEAD"
1511
+ }),
1512
+ /* @__PURE__ */ jsx(View, { style: styles$1.grow }),
1513
+ /* @__PURE__ */ jsxs(Text, {
1514
+ style: styles$1.green,
1515
+ children: ["+", changes?.added ?? 0]
1516
+ }),
1517
+ /* @__PURE__ */ jsxs(Text, {
1518
+ style: styles$1.red,
1519
+ children: ["-", changes?.removed ?? 0]
1520
+ })
1521
+ ]
1522
+ }),
1523
+ /* @__PURE__ */ jsx(Text, {
1524
+ style: [styles$1.dim, styles$1.mono],
1525
+ children: changes?.base ? `vs ${changes.base}` : "Uncommitted changes"
1526
+ }),
1527
+ /* @__PURE__ */ jsx(Text, {
1528
+ style: styles$1.dim,
1529
+ children: upstreamSummary(changes?.upstream)
1530
+ }),
1531
+ /* @__PURE__ */ jsx(View, {
1532
+ style: styles$1.inline,
1533
+ children: [
1534
+ "pull",
1535
+ "push",
1536
+ "rebase"
1537
+ ].map((action) => /* @__PURE__ */ jsx(Button, {
1538
+ variant: suggested === action ? "outline" : "ghost",
1539
+ style: suggested === action ? [styles$1.action, styles$1.suggested] : styles$1.action,
1540
+ disabled: busy || changes?.rebase !== void 0 || (action === "pull" ? changes?.upstream === void 0 : action === "push" ? changes?.branch === void 0 : changes?.base === void 0),
1541
+ onPress: () => onSync?.(action),
1542
+ children: action === "pull" ? "Pull" : action === "push" ? "Push" : "Rebase"
1543
+ }, action))
1544
+ })
1545
+ ]
1546
+ });
1547
+ }
1548
+ function FileList({ files, comments = [], onFile }) {
1549
+ return /* @__PURE__ */ jsxs(View, {
1550
+ style: styles$1.list,
1551
+ children: [/* @__PURE__ */ jsxs(Text, {
1552
+ style: styles$1.section,
1553
+ children: [files.length, " files"]
1554
+ }), files.map((file) => /* @__PURE__ */ jsxs(Pressable, {
1555
+ accessibilityRole: "button",
1556
+ testID: `git-review-file-${file.path}`,
1557
+ onPress: () => onFile?.(file.path),
1558
+ style: styles$1.fileRow,
1559
+ children: [
1560
+ /* @__PURE__ */ jsx(Text, {
1561
+ style: file.status === "added" ? styles$1.green : file.status === "untracked" ? styles$1.blue : styles$1.yellow,
1562
+ children: file.status[0]?.toUpperCase()
1563
+ }),
1564
+ /* @__PURE__ */ jsxs(View, {
1565
+ style: styles$1.grow,
1566
+ children: [/* @__PURE__ */ jsx(Text, {
1567
+ style: styles$1.body,
1568
+ children: file.path.split("/").at(-1)
1569
+ }), /* @__PURE__ */ jsx(Text, {
1570
+ numberOfLines: 1,
1571
+ style: [styles$1.dim, styles$1.mono],
1572
+ children: file.path.split("/").slice(0, -1).join("/") || "."
1573
+ })]
1574
+ }),
1575
+ /* @__PURE__ */ jsxs(Text, { children: [
1576
+ /* @__PURE__ */ jsxs(Text, {
1577
+ style: styles$1.green,
1578
+ children: ["+", file.added]
1579
+ }),
1580
+ " ",
1581
+ /* @__PURE__ */ jsxs(Text, {
1582
+ style: styles$1.red,
1583
+ children: ["-", file.removed]
1584
+ })
1585
+ ] }),
1586
+ comments.some((comment) => comment.path === file.path) ? /* @__PURE__ */ jsxs(View, {
1587
+ style: styles$1.inline,
1588
+ children: [/* @__PURE__ */ jsx(MessageIcon, {
1589
+ size: 12,
1590
+ color: "blue"
1591
+ }), /* @__PURE__ */ jsx(Text, {
1592
+ style: styles$1.blue,
1593
+ children: comments.filter((comment) => comment.path === file.path).length
1594
+ })]
1595
+ }) : null,
1596
+ /* @__PURE__ */ jsx(ChevronRightIcon, {
1597
+ size: 16,
1598
+ color: "dim"
1599
+ })
1600
+ ]
1601
+ }, file.path))]
1602
+ });
1603
+ }
1604
+ function RebaseCard({ confirmation = false, conflicts, busy = false, onResolve, onAbort }) {
1605
+ return /* @__PURE__ */ jsxs(View, {
1606
+ style: [styles$1.card, styles$1.caution],
1607
+ children: [
1608
+ /* @__PURE__ */ jsxs(View, {
1609
+ style: styles$1.inline,
1610
+ children: [/* @__PURE__ */ jsx(AlertIcon, {
1611
+ size: 20,
1612
+ color: "yellow"
1613
+ }), /* @__PURE__ */ jsx(Text, {
1614
+ style: styles$1.section,
1615
+ children: "Rebase paused"
1616
+ })]
1617
+ }),
1618
+ /* @__PURE__ */ jsx(Text, {
1619
+ style: styles$1.body,
1620
+ children: "Resolve conflicts or ask Po to do it for you."
1621
+ }),
1622
+ /* @__PURE__ */ jsx(Text, {
1623
+ style: [styles$1.dim, styles$1.mono],
1624
+ children: conflicts.join("\n")
1625
+ }),
1626
+ /* @__PURE__ */ jsx(Action, {
1627
+ disabled: busy,
1628
+ onPress: onResolve,
1629
+ variant: confirmation ? "outline" : "primary",
1630
+ children: "Ask Po to resolve"
1631
+ }),
1632
+ /* @__PURE__ */ jsx(Action, {
1633
+ disabled: busy,
1634
+ onPress: onAbort,
1635
+ variant: "danger-outline",
1636
+ children: "Abort rebase"
1637
+ })
1638
+ ]
1639
+ });
1640
+ }
1641
+ function FileHeader({ entry, index, total, onPrevious, onNext, onChanges }) {
1642
+ return /* @__PURE__ */ jsxs(View, {
1643
+ style: styles$1.content,
1644
+ children: [
1645
+ /* @__PURE__ */ jsx(Text, {
1646
+ style: styles$1.heading,
1647
+ children: entry.path.split("/").at(-1)
1648
+ }),
1649
+ /* @__PURE__ */ jsx(Text, {
1650
+ style: [styles$1.dim, styles$1.mono],
1651
+ children: entry.path.split("/").slice(0, -1).join("/") || "."
1652
+ }),
1653
+ /* @__PURE__ */ jsxs(View, {
1654
+ style: styles$1.inline,
1655
+ children: [
1656
+ /* @__PURE__ */ jsxs(Text, {
1657
+ style: styles$1.dim,
1658
+ children: [entry.status[0]?.toUpperCase(), entry.status.slice(1)]
1659
+ }),
1660
+ /* @__PURE__ */ jsxs(Text, {
1661
+ style: styles$1.green,
1662
+ children: ["+", entry.added]
1663
+ }),
1664
+ /* @__PURE__ */ jsxs(Text, {
1665
+ style: styles$1.red,
1666
+ children: ["-", entry.removed]
1667
+ })
1668
+ ]
1669
+ }),
1670
+ /* @__PURE__ */ jsxs(View, {
1671
+ style: styles$1.navigation,
1672
+ children: [
1673
+ /* @__PURE__ */ jsx(Action, {
1674
+ variant: "ghost",
1675
+ disabled: index === 0,
1676
+ onPress: onPrevious,
1677
+ children: "Previous"
1678
+ }),
1679
+ /* @__PURE__ */ jsx(Pressable, {
1680
+ accessibilityRole: "button",
1681
+ accessibilityLabel: "Return to changes",
1682
+ onPress: onChanges,
1683
+ style: [styles$1.inline, styles$1.action],
1684
+ children: /* @__PURE__ */ jsxs(Text, {
1685
+ style: styles$1.dim,
1686
+ children: [
1687
+ onChanges ? "Changes · " : "",
1688
+ index + 1,
1689
+ " of ",
1690
+ total
1691
+ ]
1692
+ })
1693
+ }),
1694
+ /* @__PURE__ */ jsx(Action, {
1695
+ variant: "ghost",
1696
+ disabled: index === total - 1,
1697
+ onPress: onNext,
1698
+ children: "Next"
1699
+ })
1700
+ ]
1701
+ })
1702
+ ]
1703
+ });
1704
+ }
1705
+ const DiffRows = memo(function DiffRows({ rows, comments = [], selection, onLine, showInlineComments = true, gutterDigits }) {
1706
+ const anchoredComments = useMemo(() => {
1707
+ const index = /* @__PURE__ */ new Map();
1708
+ for (const comment of comments) {
1709
+ const key = `${comment.side}:${comment.endLine ?? comment.startLine}`;
1710
+ const entries = index.get(key);
1711
+ if (entries) entries.push(comment);
1712
+ else index.set(key, [comment]);
1713
+ }
1714
+ return index;
1715
+ }, [comments]);
1716
+ const digits = gutterDigits ?? rows.reduce((max, row) => Math.max(max, String(row.line).length), 1);
1717
+ return /* @__PURE__ */ jsx(View, { children: rows.map((row, i) => {
1718
+ const side = row.marker === "-" ? "old" : "new";
1719
+ const anchored = anchoredComments.get(`${side}:${row.line}`) ?? [];
1720
+ return /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Pressable, {
1721
+ accessibilityRole: "button",
1722
+ accessibilityLabel: `Select ${side} line ${row.line}`,
1723
+ onPress: () => onLine?.(row),
1724
+ style: [
1725
+ styles$1.diffRow,
1726
+ row.marker === "+" && styles$1.added,
1727
+ row.marker === "-" && styles$1.removed,
1728
+ selection?.side === side && row.line >= selection.startLine && row.line <= selection.endLine && styles$1.selection
1729
+ ],
1730
+ children: [
1731
+ /* @__PURE__ */ jsx(Text, {
1732
+ numberOfLines: 1,
1733
+ style: [styles$1.mono, styles$1.gutter],
1734
+ children: String(row.line).padStart(digits, "\xA0")
1735
+ }),
1736
+ /* @__PURE__ */ jsx(Text, {
1737
+ style: [styles$1.dim, styles$1.mono],
1738
+ children: row.marker
1739
+ }),
1740
+ /* @__PURE__ */ jsx(SyntaxLine, {
1741
+ text: row.content || " ",
1742
+ style: [styles$1.mono, styles$1.code]
1743
+ }),
1744
+ anchored.length ? /* @__PURE__ */ jsx(MessageIcon, {
1745
+ size: 12,
1746
+ color: "blue"
1747
+ }) : null
1748
+ ]
1749
+ }), showInlineComments && anchored.map((comment) => /* @__PURE__ */ jsxs(View, {
1750
+ style: styles$1.inlineComment,
1751
+ children: [/* @__PURE__ */ jsxs(Text, {
1752
+ style: styles$1.blue,
1753
+ children: ["Queued comment · ", lineLabel(comment)]
1754
+ }), /* @__PURE__ */ jsx(Text, {
1755
+ style: styles$1.body,
1756
+ children: comment.body
1757
+ })]
1758
+ }, comment.id))] }, i);
1759
+ }) });
1760
+ });
1761
+ function lineLabel(comment) {
1762
+ return comment.startLine === void 0 ? "File comment" : comment.endLine === comment.startLine || comment.endLine === void 0 ? `Line ${comment.startLine}` : `Lines ${comment.startLine} to ${comment.endLine}`;
1763
+ }
1764
+ function CommentSheet({ range, snippet, value, onChange, onAdd, onCancel }) {
1765
+ return /* @__PURE__ */ jsxs(View, {
1766
+ style: styles$1.sheet,
1767
+ children: [
1768
+ /* @__PURE__ */ jsx(View, { style: styles$1.handle }),
1769
+ /* @__PURE__ */ jsx(Text, {
1770
+ style: styles$1.sheetTitle,
1771
+ children: range ? lineLabel(range) : "Comment on file"
1772
+ }),
1773
+ snippet ? /* @__PURE__ */ jsx(View, {
1774
+ style: styles$1.quote,
1775
+ children: /* @__PURE__ */ jsx(SyntaxLine, {
1776
+ text: snippet,
1777
+ style: [styles$1.body, styles$1.mono]
1778
+ })
1779
+ }) : null,
1780
+ /* @__PURE__ */ jsx(Textarea, {
1781
+ accessibilityLabel: "What should change here?",
1782
+ placeholder: "What should change here?",
1783
+ rows: 4,
1784
+ value,
1785
+ onChangeText: onChange
1786
+ }),
1787
+ /* @__PURE__ */ jsx(Action, {
1788
+ variant: "primary",
1789
+ disabled: value !== void 0 && value.trim() === "",
1790
+ onPress: onAdd,
1791
+ children: "Add comment"
1792
+ }),
1793
+ /* @__PURE__ */ jsx(Action, {
1794
+ variant: "ghost",
1795
+ onPress: onCancel,
1796
+ children: "Cancel"
1797
+ })
1798
+ ]
1799
+ });
1800
+ }
1801
+ function CommentCards({ comments, onRemove }) {
1802
+ return /* @__PURE__ */ jsxs(View, {
1803
+ style: styles$1.content,
1804
+ children: [/* @__PURE__ */ jsxs(Text, {
1805
+ style: styles$1.body,
1806
+ children: [
1807
+ comments.length === 2 ? "Two" : comments.length,
1808
+ " ",
1809
+ comments.length === 1 ? "comment" : "comments",
1810
+ " queued for Po, whenever you are ready."
1811
+ ]
1812
+ }), comments.map((comment) => /* @__PURE__ */ jsxs(View, {
1813
+ style: styles$1.card,
1814
+ children: [
1815
+ /* @__PURE__ */ jsx(Text, {
1816
+ style: [styles$1.body, styles$1.mono],
1817
+ children: comment.relPath
1818
+ }),
1819
+ /* @__PURE__ */ jsx(Text, {
1820
+ style: styles$1.dim,
1821
+ children: lineLabel(comment)
1822
+ }),
1823
+ comment.snippet ? /* @__PURE__ */ jsx(View, {
1824
+ style: styles$1.quote,
1825
+ children: /* @__PURE__ */ jsx(SyntaxLine, {
1826
+ text: comment.snippet,
1827
+ style: [styles$1.body, styles$1.mono]
1828
+ })
1829
+ }) : null,
1830
+ /* @__PURE__ */ jsx(Text, {
1831
+ style: styles$1.body,
1832
+ children: comment.body
1833
+ }),
1834
+ /* @__PURE__ */ jsx(View, {
1835
+ style: styles$1.remove,
1836
+ children: /* @__PURE__ */ jsx(Action, {
1837
+ variant: "ghost",
1838
+ onPress: () => onRemove?.(comment.id),
1839
+ children: "Remove"
1840
+ })
1841
+ })
1842
+ ]
1843
+ }, comment.id))]
1844
+ });
1845
+ }
1846
+ const styles$1 = StyleSheet.create((theme) => ({
1847
+ heading: {
1848
+ fontFamily: fontOf(theme.fontFamily.sans),
1849
+ fontSize: theme.fontSize["2xl"],
1850
+ fontWeight: "600",
1851
+ color: theme.colors.hi,
1852
+ uni__dependencies: [0]
1853
+ },
1854
+ sheetTitle: {
1855
+ fontFamily: fontOf(theme.fontFamily.sans),
1856
+ fontSize: theme.fontSize.xl,
1857
+ fontWeight: "600",
1858
+ color: theme.colors.hi,
1859
+ uni__dependencies: [0]
1860
+ },
1861
+ section: {
1862
+ fontFamily: fontOf(theme.fontFamily.sans),
1863
+ fontSize: theme.fontSize.lg,
1864
+ fontWeight: "600",
1865
+ color: theme.colors.hi,
1866
+ uni__dependencies: [0]
1867
+ },
1868
+ body: {
1869
+ fontFamily: fontOf(theme.fontFamily.sans),
1870
+ fontSize: theme.fontSize.base,
1871
+ lineHeight: theme.lineHeightRelaxed.base,
1872
+ color: theme.colors.text,
1873
+ uni__dependencies: [0]
1874
+ },
1875
+ dim: {
1876
+ fontFamily: fontOf(theme.fontFamily.sans),
1877
+ fontSize: theme.fontSize.sm,
1878
+ lineHeight: theme.lineHeightRelaxed.sm,
1879
+ color: theme.colors.dim,
1880
+ uni__dependencies: [0]
1881
+ },
1882
+ mono: {
1883
+ fontFamily: fontOf(theme.fontFamily.mono),
1884
+ fontSize: theme.fontSize.sm,
1885
+ lineHeight: theme.lineHeightRelaxed.sm,
1886
+ uni__dependencies: [0]
1887
+ },
1888
+ branch: {
1889
+ fontFamily: fontOf(theme.fontFamily.mono),
1890
+ fontSize: theme.fontSize.lg,
1891
+ fontWeight: "600",
1892
+ color: theme.colors.hi,
1893
+ uni__dependencies: [0]
1894
+ },
1895
+ green: {
1896
+ fontSize: theme.fontSize.sm,
1897
+ color: theme.colors.green,
1898
+ uni__dependencies: [0]
1899
+ },
1900
+ red: {
1901
+ fontSize: theme.fontSize.sm,
1902
+ color: theme.colors.red,
1903
+ uni__dependencies: [0]
1904
+ },
1905
+ blue: {
1906
+ fontFamily: fontOf(theme.fontFamily.sans),
1907
+ fontSize: theme.fontSize.sm,
1908
+ color: theme.colors.blue,
1909
+ uni__dependencies: [0]
1910
+ },
1911
+ yellow: {
1912
+ fontSize: theme.fontSize.sm,
1913
+ color: theme.colors.yellow,
1914
+ uni__dependencies: [0]
1915
+ },
1916
+ content: {
1917
+ padding: theme.spacing(4),
1918
+ gap: theme.spacing(3),
1919
+ uni__dependencies: [0]
1920
+ },
1921
+ card: {
1922
+ borderWidth: 1,
1923
+ borderColor: theme.colors.border,
1924
+ borderRadius: theme.borderRadius.lg,
1925
+ padding: theme.spacing(3),
1926
+ gap: theme.spacing(2),
1927
+ uni__dependencies: [0]
1928
+ },
1929
+ inline: {
1930
+ flexDirection: "row",
1931
+ alignItems: "center",
1932
+ gap: theme.spacing(2),
1933
+ uni__dependencies: [0]
1934
+ },
1935
+ grow: {
1936
+ flex: 1,
1937
+ minWidth: 0
1938
+ },
1939
+ action: { minHeight: 44 },
1940
+ primary: {
1941
+ backgroundColor: theme.colors.green,
1942
+ uni__dependencies: [0]
1943
+ },
1944
+ primaryLabel: {
1945
+ fontFamily: fontOf(theme.fontFamily.sans),
1946
+ fontSize: theme.fontSize.base,
1947
+ fontWeight: "600",
1948
+ color: theme.scheme === "dark" ? theme.colors.rail : theme.colors["on-selected"],
1949
+ uni__dependencies: [0]
1950
+ },
1951
+ suggested: {
1952
+ backgroundColor: theme.colors["tint-green"],
1953
+ borderColor: theme.colors["edge-green"],
1954
+ flex: 1,
1955
+ uni__dependencies: [0]
1956
+ },
1957
+ list: {
1958
+ gap: theme.spacing(2),
1959
+ uni__dependencies: [0]
1960
+ },
1961
+ fileRow: {
1962
+ minHeight: 44,
1963
+ paddingVertical: theme.spacing(2),
1964
+ flexDirection: "row",
1965
+ alignItems: "center",
1966
+ gap: theme.spacing(2),
1967
+ borderBottomWidth: StyleSheet.hairlineWidth,
1968
+ borderColor: theme.colors.border,
1969
+ uni__dependencies: [0]
1970
+ },
1971
+ footer: {
1972
+ minHeight: 76,
1973
+ padding: theme.spacing(4),
1974
+ flexDirection: "row",
1975
+ alignItems: "center",
1976
+ justifyContent: "space-between",
1977
+ gap: theme.spacing(2),
1978
+ borderTopWidth: 1,
1979
+ borderColor: theme.colors.border,
1980
+ backgroundColor: theme.colors.rail,
1981
+ uni__dependencies: [0]
1982
+ },
1983
+ caution: {
1984
+ backgroundColor: theme.colors["tint-yellow"],
1985
+ borderColor: theme.colors["edge-yellow"],
1986
+ uni__dependencies: [0]
1987
+ },
1988
+ failure: {
1989
+ backgroundColor: theme.colors["tint-red"],
1990
+ borderColor: theme.colors["edge-red"],
1991
+ uni__dependencies: [0]
1992
+ },
1993
+ sheet: {
1994
+ padding: theme.spacing(4),
1995
+ gap: theme.spacing(3),
1996
+ borderTopWidth: 1,
1997
+ borderColor: theme.colors.border,
1998
+ borderTopLeftRadius: theme.borderRadius.lg,
1999
+ borderTopRightRadius: theme.borderRadius.lg,
2000
+ backgroundColor: theme.colors.rail,
2001
+ uni__dependencies: [0]
2002
+ },
2003
+ handle: {
2004
+ height: 4,
2005
+ width: 40,
2006
+ borderRadius: theme.borderRadius.md,
2007
+ backgroundColor: theme.colors.border,
2008
+ alignSelf: "center",
2009
+ uni__dependencies: [0]
2010
+ },
2011
+ empty: {
2012
+ flex: 1,
2013
+ alignItems: "center",
2014
+ justifyContent: "center",
2015
+ gap: theme.spacing(3),
2016
+ padding: theme.spacing(6),
2017
+ uni__dependencies: [0]
2018
+ },
2019
+ navigation: {
2020
+ flexDirection: "row",
2021
+ justifyContent: "space-between",
2022
+ alignItems: "center"
2023
+ },
2024
+ diffRow: {
2025
+ flexDirection: "row",
2026
+ alignItems: "flex-start",
2027
+ gap: theme.spacing(2),
2028
+ paddingHorizontal: theme.spacing(2),
2029
+ paddingVertical: theme.spacing(1),
2030
+ borderLeftWidth: 3,
2031
+ borderLeftColor: theme.colors.bg,
2032
+ uni__dependencies: [0]
2033
+ },
2034
+ gutter: {
2035
+ color: theme.colors.dim,
2036
+ width: "auto",
2037
+ minWidth: 24,
2038
+ flexShrink: 0,
2039
+ textAlign: "right",
2040
+ uni__dependencies: [0]
2041
+ },
2042
+ disclosure: {
2043
+ padding: theme.spacing(3),
2044
+ gap: theme.spacing(2),
2045
+ borderTopWidth: 1,
2046
+ borderColor: theme.colors.border,
2047
+ backgroundColor: theme.colors.bg,
2048
+ uni__dependencies: [0]
2049
+ },
2050
+ code: {
2051
+ flex: 1,
2052
+ minWidth: 0,
2053
+ color: theme.colors.text,
2054
+ uni__dependencies: [0]
2055
+ },
2056
+ added: {
2057
+ backgroundColor: theme.colors["tint-green"],
2058
+ uni__dependencies: [0]
2059
+ },
2060
+ removed: {
2061
+ backgroundColor: theme.colors["tint-red"],
2062
+ uni__dependencies: [0]
2063
+ },
2064
+ selection: {
2065
+ borderLeftColor: theme.colors.green,
2066
+ uni__dependencies: [0]
2067
+ },
2068
+ inlineComment: {
2069
+ margin: theme.spacing(2),
2070
+ padding: theme.spacing(3),
2071
+ borderWidth: 1,
2072
+ borderColor: theme.colors["edge-blue"],
2073
+ borderRadius: theme.borderRadius.md,
2074
+ gap: theme.spacing(1),
2075
+ uni__dependencies: [0]
2076
+ },
2077
+ quote: {
2078
+ padding: theme.spacing(2),
2079
+ backgroundColor: theme.colors.panel,
2080
+ borderRadius: theme.borderRadius.md,
2081
+ uni__dependencies: [0]
2082
+ },
2083
+ remove: { alignSelf: "flex-end" }
2084
+ }));
2085
+ const DIFF_ROW_STEP = 200;
2086
+ function GitReviewView(props) {
2087
+ const { screen, activity, summary, file, path, comments } = props;
2088
+ const rows = useMemo(() => file?.hunks.flatMap((hunk) => hunk.rows) ?? [], [file?.hunks]);
2089
+ const gutterDigits = useMemo(() => rows.reduce((max, row) => Math.max(max, String(row.line).length), 1), [rows]);
2090
+ const fileComments = useMemo(() => comments.filter((comment) => comment.path === path), [comments, path]);
2091
+ const initialLimit = props.initialVisibleRows ?? DIFF_ROW_STEP;
2092
+ const [disclosure, setDisclosure] = useState({
2093
+ rows,
2094
+ path,
2095
+ limit: initialLimit
2096
+ });
2097
+ const current = disclosure.rows === rows && disclosure.path === path ? disclosure : {
2098
+ rows,
2099
+ path,
2100
+ limit: initialLimit
2101
+ };
2102
+ if (current !== disclosure) setDisclosure(current);
2103
+ const visibleRows = useMemo(() => rows.slice(0, current.limit), [rows, current.limit]);
2104
+ const index = summary?.files.findIndex((entry) => entry.path === path) ?? -1;
2105
+ const entry = summary?.files[index];
2106
+ return /* @__PURE__ */ jsxs(View, {
2107
+ style: styles$1.grow,
2108
+ children: [
2109
+ screen === "review" ? /* @__PURE__ */ jsxs(View, {
2110
+ style: styles$1.inline,
2111
+ children: [/* @__PURE__ */ jsx(Action, {
2112
+ variant: "ghost",
2113
+ onPress: () => props.onScreen("changes"),
2114
+ children: "Changes"
2115
+ }), screen === "review" && path ? /* @__PURE__ */ jsx(Action, {
2116
+ variant: "ghost",
2117
+ onPress: () => props.onScreen("file"),
2118
+ children: "File diff"
2119
+ }) : null]
2120
+ }) : null,
2121
+ /* @__PURE__ */ jsxs(ScrollView, {
2122
+ style: styles$1.grow,
2123
+ children: [screen === "changes" ? /* @__PURE__ */ jsxs(View, {
2124
+ style: styles$1.content,
2125
+ children: [
2126
+ summary?.repository !== false ? /* @__PURE__ */ jsx(SyncCard, {
2127
+ changes: activity.changes,
2128
+ busy: activity.pending !== void 0,
2129
+ onSync: props.onSync
2130
+ }) : null,
2131
+ activity.pending ? /* @__PURE__ */ jsx(Text, {
2132
+ style: styles$1.dim,
2133
+ children: activity.pending
2134
+ }) : null,
2135
+ activity.error ? /* @__PURE__ */ jsxs(View, {
2136
+ style: [styles$1.card, props.forceRequired ? styles$1.caution : styles$1.failure],
2137
+ children: [
2138
+ /* @__PURE__ */ jsx(Text, {
2139
+ style: styles$1.body,
2140
+ children: activity.error
2141
+ }),
2142
+ props.forceRequired ? /* @__PURE__ */ jsx(Action, {
2143
+ variant: "danger-outline",
2144
+ onPress: props.onForce,
2145
+ children: "Force push with lease"
2146
+ }) : null,
2147
+ /* @__PURE__ */ jsx(Action, {
2148
+ variant: "ghost",
2149
+ onPress: props.onDismiss,
2150
+ children: "Dismiss"
2151
+ })
2152
+ ]
2153
+ }) : null,
2154
+ activity.changes?.rebase ? /* @__PURE__ */ jsx(RebaseCard, {
2155
+ conflicts: activity.changes.rebase.conflicts,
2156
+ busy: activity.pending !== void 0,
2157
+ onResolve: props.onResolve,
2158
+ onAbort: props.onAbort
2159
+ }) : null,
2160
+ !summary ? /* @__PURE__ */ jsx(Text, {
2161
+ style: styles$1.dim,
2162
+ children: "Loading changes…"
2163
+ }) : summary.files.length ? /* @__PURE__ */ jsx(FileList, {
2164
+ files: summary.files,
2165
+ comments,
2166
+ onFile: props.onFile
2167
+ }) : /* @__PURE__ */ jsxs(View, {
2168
+ style: styles$1.empty,
2169
+ children: [/* @__PURE__ */ jsx(Text, {
2170
+ style: styles$1.sheetTitle,
2171
+ children: summary.repository ? "No changes yet" : "Not a Git checkout"
2172
+ }), /* @__PURE__ */ jsx(Text, {
2173
+ style: styles$1.dim,
2174
+ children: summary.repository ? `Nothing to review on ${summary.branch ?? "this checkout"}.` : "This folder is not a Git repository, so there is nothing to sync."
2175
+ })]
2176
+ }),
2177
+ summary?.truncated ? /* @__PURE__ */ jsx(Text, {
2178
+ style: styles$1.dim,
2179
+ children: "The file list was truncated."
2180
+ }) : null
2181
+ ]
2182
+ }) : screen === "file" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2183
+ entry ? /* @__PURE__ */ jsx(FileHeader, {
2184
+ entry,
2185
+ index,
2186
+ total: summary?.files.length ?? 0,
2187
+ onChanges: () => props.onScreen("changes"),
2188
+ onPrevious: () => {
2189
+ const previous = summary?.files[index - 1];
2190
+ if (previous) props.onFile(previous.path);
2191
+ },
2192
+ onNext: () => {
2193
+ const next = summary?.files[index + 1];
2194
+ if (next) props.onFile(next.path);
2195
+ }
2196
+ }) : /* @__PURE__ */ jsx(Text, {
2197
+ style: styles$1.heading,
2198
+ children: path
2199
+ }),
2200
+ !file ? /* @__PURE__ */ jsxs(View, {
2201
+ style: styles$1.content,
2202
+ children: [/* @__PURE__ */ jsx(Text, {
2203
+ style: styles$1.dim,
2204
+ children: props.error ? "The diff could not be loaded. Please try again." : "Loading diff…"
2205
+ }), props.error && path ? /* @__PURE__ */ jsx(Action, {
2206
+ onPress: () => props.onFile(path),
2207
+ children: "Retry"
2208
+ }) : null]
2209
+ }) : file.binary || file.tooLarge ? /* @__PURE__ */ jsx(View, {
2210
+ style: styles$1.content,
2211
+ children: /* @__PURE__ */ jsx(Text, {
2212
+ style: styles$1.section,
2213
+ children: file.binary ? "Binary file" : "Diff too large to display"
2214
+ })
2215
+ }) : /* @__PURE__ */ jsx(DiffRows, {
2216
+ rows: visibleRows,
2217
+ comments: fileComments,
2218
+ selection: props.selection,
2219
+ onLine: props.onLine,
2220
+ gutterDigits
2221
+ }),
2222
+ fileComments.filter((comment) => comment.startLine === void 0).map((comment) => /* @__PURE__ */ jsxs(View, {
2223
+ style: styles$1.inlineComment,
2224
+ children: [/* @__PURE__ */ jsx(Text, {
2225
+ style: styles$1.blue,
2226
+ children: "Queued file comment"
2227
+ }), /* @__PURE__ */ jsx(Text, {
2228
+ style: styles$1.body,
2229
+ children: comment.body
2230
+ })]
2231
+ }, comment.id)),
2232
+ /* @__PURE__ */ jsx(View, {
2233
+ style: styles$1.content,
2234
+ children: /* @__PURE__ */ jsx(Action, {
2235
+ onPress: props.onComment,
2236
+ children: "Comment on file"
2237
+ })
2238
+ })
2239
+ ] }) : /* @__PURE__ */ jsx(CommentCards, {
2240
+ comments,
2241
+ onRemove: props.onRemove
2242
+ }), props.error ? /* @__PURE__ */ jsx(Text, {
2243
+ style: [styles$1.red, styles$1.content],
2244
+ children: props.error
2245
+ }) : null]
2246
+ }, `${screen}:${path ?? ""}`),
2247
+ /* @__PURE__ */ jsx(Footer, {
2248
+ count: comments.length,
2249
+ confirmation: screen === "changes" && activity.changes?.rebase !== void 0,
2250
+ discard: screen === "review",
2251
+ onReview: () => props.onScreen("review"),
2252
+ onSend: props.onSend,
2253
+ onDiscard: props.onDiscard
2254
+ }),
2255
+ screen === "file" && file && !file.binary && !file.tooLarge ? /* @__PURE__ */ jsxs(View, {
2256
+ style: styles$1.disclosure,
2257
+ children: [/* @__PURE__ */ jsxs(Text, {
2258
+ style: styles$1.dim,
2259
+ accessibilityLiveRegion: "polite",
2260
+ children: [
2261
+ Math.min(current.limit, rows.length),
2262
+ " of ",
2263
+ rows.length,
2264
+ " diff rows shown",
2265
+ props.selection ? ` · ${props.selection.side} line ${props.selection.startLine} selected` : ""
2266
+ ]
2267
+ }), current.limit < rows.length ? /* @__PURE__ */ jsxs(Button, {
2268
+ variant: "outline",
2269
+ onPress: () => setDisclosure({
2270
+ ...current,
2271
+ limit: Math.min(rows.length, current.limit + DIFF_ROW_STEP)
2272
+ }),
2273
+ children: [
2274
+ "Show ",
2275
+ Math.min(DIFF_ROW_STEP, rows.length - current.limit),
2276
+ " more rows"
2277
+ ]
2278
+ }) : /* @__PURE__ */ jsx(Text, {
2279
+ style: styles$1.dim,
2280
+ children: "All diff rows shown"
2281
+ })]
2282
+ }) : null
2283
+ ]
2284
+ });
2285
+ }
2286
+ //#endregion
2287
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/GitReviewPanel.native.tsx
2288
+ function useGitChanges(sessionId) {
2289
+ const subscribe = useCallback((listener) => {
2290
+ const sub = gitChanges.store.subscribe(listener);
2291
+ return () => sub.unsubscribe();
2292
+ }, []);
2293
+ return useSyncExternalStore(subscribe, () => gitChanges.select(gitChanges.store.state, sessionId));
2294
+ }
2295
+ function reviewTab() {
2296
+ return {
2297
+ id: "git-review",
2298
+ label: "review",
2299
+ panel: GitReviewPanel
2300
+ };
2301
+ }
2302
+ function GitReviewPanel(props) {
2303
+ return /* @__PURE__ */ jsx(GitReviewPanelSession, { ...props }, props.sessionId);
2304
+ }
2305
+ function GitReviewPanelSession({ sessionId, sendSessionFrame }) {
2306
+ const activity = useGitChanges(sessionId);
2307
+ const subscribe = useCallback((listener) => {
2308
+ const sub = gitReview.store.subscribe(listener);
2309
+ return () => sub.unsubscribe();
2310
+ }, []);
2311
+ const comments = useSyncExternalStore(subscribe, () => gitReview.select(gitReview.store.state, sessionId).comments);
2312
+ const [summary, setSummary] = useState();
2313
+ const [file, setFile] = useState();
2314
+ const [error, setError] = useState();
2315
+ const [dismissedError, setDismissedError] = useState();
2316
+ const [confirm, setConfirm] = useState();
2317
+ const [screen, setScreen] = useState("changes");
2318
+ const [path, setPath] = useState();
2319
+ const [selection, setSelection] = useState();
2320
+ const [draft, setDraft] = useState();
2321
+ const request = useRef(void 0);
2322
+ const fingerprint = JSON.stringify(activity.changes);
2323
+ useEffect(() => {
2324
+ if (sessionId === null) return void 0;
2325
+ const controller = new AbortController();
2326
+ fetchReviewSummary(sessionId, controller.signal).then((result) => {
2327
+ if (!controller.signal.aborted) {
2328
+ if (result.ok) setSummary(result.summary);
2329
+ else setError(result.error);
2330
+ }
2331
+ }).catch((cause) => {
2332
+ if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
2333
+ });
2334
+ return () => {
2335
+ controller.abort();
2336
+ request.current?.abort();
2337
+ };
2338
+ }, [sessionId, fingerprint]);
2339
+ const loadFile = async (path) => {
2340
+ if (sessionId === null) return;
2341
+ setScreen("file");
2342
+ setPath(path);
2343
+ setSelection(void 0);
2344
+ setDraft(void 0);
2345
+ request.current?.abort();
2346
+ const controller = new AbortController();
2347
+ request.current = controller;
2348
+ setFile(void 0);
2349
+ setError(void 0);
2350
+ try {
2351
+ const result = await fetchReviewFile(sessionId, path, controller.signal);
2352
+ if (!controller.signal.aborted) {
2353
+ if (result.ok) setFile(result.diff);
2354
+ else setError(result.error);
2355
+ }
2356
+ } catch (cause) {
2357
+ if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
2358
+ }
2359
+ };
2360
+ if (sessionId === null) return null;
2361
+ const changes = activity.changes;
2362
+ const send = (message, onSent) => {
2363
+ const failure = deliverToAgent(sendSessionFrame, sessionId, message);
2364
+ setError(failure);
2365
+ if (failure === void 0) onSent?.();
2366
+ };
2367
+ const sync = (action) => requestGitSync(sessionId, action);
2368
+ const forceRequired = activity.error !== dismissedError && activity.errorTarget?.action === "push" && activity.errorTarget.forceRequired === true;
2369
+ const sendReview = () => {
2370
+ if (comments.length === 0) return;
2371
+ const body = comments.map((item) => {
2372
+ return `### ${item.startLine === void 0 ? item.relPath : item.side === "old" ? `${item.relPath} (removed lines ${item.startLine}-${item.endLine ?? item.startLine})` : `${item.relPath}:${item.startLine}-${item.endLine ?? item.startLine}`}\n\n${item.snippet.split("\n").slice(0, 20).join("\n").slice(0, 2e3).split("\n").map((line) => `> ${line}`).join("\n")}\n\n${item.body.trim()}`;
2373
+ }).join("\n\n");
2374
+ send(`${reviewHeading(comments.length, summary?.branch, summary?.base)}\n\n${body}`, () => clearReviewComments(sessionId));
2375
+ };
2376
+ const selectLine = (row) => {
2377
+ if (!file) return;
2378
+ const next = selectReviewLine(selection, row);
2379
+ setSelection(next.range);
2380
+ if (next.complete) {
2381
+ const rows = reviewRangeRows(file.hunks.flatMap((hunk) => hunk.rows), next.range);
2382
+ setDraft({
2383
+ path: file.path,
2384
+ range: next.range,
2385
+ snippet: rows.map((item) => item.content).join("\n")
2386
+ });
2387
+ }
2388
+ };
2389
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2390
+ /* @__PURE__ */ jsx(GitReviewView, {
2391
+ screen,
2392
+ activity: {
2393
+ ...activity,
2394
+ error: activity.error === dismissedError ? void 0 : activity.error
2395
+ },
2396
+ summary,
2397
+ file,
2398
+ path,
2399
+ comments,
2400
+ error,
2401
+ forceRequired,
2402
+ selection,
2403
+ onScreen: setScreen,
2404
+ onFile: (path) => void loadFile(path),
2405
+ onLine: selectLine,
2406
+ onSync: sync,
2407
+ onForce: () => setConfirm("force"),
2408
+ onAbort: () => setConfirm("abort"),
2409
+ onResolve: () => send(conflictPrompt(changes?.branch, changes?.rebase?.conflicts ?? [])),
2410
+ onDismiss: () => setDismissedError(activity.error),
2411
+ onComment: () => {
2412
+ if (path) {
2413
+ setSelection(void 0);
2414
+ setDraft({
2415
+ path,
2416
+ snippet: ""
2417
+ });
2418
+ }
2419
+ },
2420
+ onRemove: (id) => removeReviewComment(sessionId, id),
2421
+ onSend: sendReview,
2422
+ onDiscard: () => setConfirm("discard")
2423
+ }),
2424
+ /* @__PURE__ */ jsx(Sheet, {
2425
+ open: draft !== void 0,
2426
+ onOpenChange: (open) => {
2427
+ if (!open) {
2428
+ setDraft(void 0);
2429
+ setSelection(void 0);
2430
+ }
2431
+ },
2432
+ children: /* @__PURE__ */ jsx(SheetContent, { children: /* @__PURE__ */ jsx(SheetBody, { children: /* @__PURE__ */ jsx(CommentEditor, {
2433
+ range: draft?.range,
2434
+ snippet: draft?.snippet ?? "",
2435
+ onCancel: () => {
2436
+ setDraft(void 0);
2437
+ setSelection(void 0);
2438
+ },
2439
+ onAdd: (comment) => {
2440
+ if (!draft || !comment.trim()) return;
2441
+ addReviewComment(sessionId, {
2442
+ path: draft.path,
2443
+ relPath: draft.path,
2444
+ body: comment.trim(),
2445
+ snippet: draft.snippet,
2446
+ ...draft.range
2447
+ });
2448
+ setDraft(void 0);
2449
+ setSelection(void 0);
2450
+ }
2451
+ }, draft ? "open" : "closed") }) })
2452
+ }),
2453
+ /* @__PURE__ */ jsx(Sheet, {
2454
+ open: confirm !== void 0,
2455
+ onOpenChange: (open) => {
2456
+ if (!open) setConfirm(void 0);
2457
+ },
2458
+ children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: confirm === "force" ? "Force push with lease?" : confirm === "abort" ? "Abort rebase?" : "Discard review?" }) }), /* @__PURE__ */ jsxs(SheetBody, { children: [
2459
+ /* @__PURE__ */ jsx(Text, {
2460
+ style: styles$1.dim,
2461
+ children: confirm === "force" ? "Git refuses this if the remote holds commits you never fetched and integrated." : confirm === "abort" ? "Abort discards any conflict resolution in progress." : "Your queued comments will be removed."
2462
+ }),
2463
+ /* @__PURE__ */ jsx(Button, {
2464
+ variant: "danger",
2465
+ disabled: confirm !== "discard" && activity.pending !== void 0,
2466
+ onPress: () => {
2467
+ if (confirm === "force") requestGitSync(sessionId, "push", true);
2468
+ else if (confirm === "abort") sync("abort-rebase");
2469
+ else clearReviewComments(sessionId);
2470
+ setConfirm(void 0);
2471
+ },
2472
+ children: confirm === "force" ? "Force push with lease" : confirm === "abort" ? "Abort rebase" : "Discard"
2473
+ }),
2474
+ /* @__PURE__ */ jsx(Button, {
2475
+ variant: "ghost",
2476
+ onPress: () => setConfirm(void 0),
2477
+ children: "Cancel"
2478
+ })
2479
+ ] })] })
2480
+ })
2481
+ ] });
2482
+ }
2483
+ function CommentEditor({ onAdd, ...props }) {
2484
+ const [body, setBody] = useState("");
2485
+ return /* @__PURE__ */ jsx(CommentSheet, {
2486
+ ...props,
2487
+ value: body,
2488
+ onChange: setBody,
2489
+ onAdd: () => onAdd(body)
2490
+ });
2491
+ }
2492
+ //#endregion
2493
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/WorktreesActivitySection.native.tsx
2494
+ function GitChangesRow({ state = "changes", branch = "Detached HEAD", added = 0, removed = 0, onReview }) {
2495
+ return /* @__PURE__ */ jsxs(Pressable, {
2496
+ testID: "activity-diff-changes",
2497
+ accessibilityRole: "button",
2498
+ accessibilityLabel: "Open changes review",
2499
+ onPress: onReview,
2500
+ style: styles.changeRow,
2501
+ children: [/* @__PURE__ */ jsxs(View, {
2502
+ style: styles.line,
2503
+ children: [
2504
+ /* @__PURE__ */ jsx(FileIcon, {
2505
+ size: 18,
2506
+ color: "dim"
2507
+ }),
2508
+ /* @__PURE__ */ jsx(Text, {
2509
+ style: [styles.heading, styles.grow],
2510
+ children: "Changes"
2511
+ }),
2512
+ /* @__PURE__ */ jsx(ChevronRightIcon, {
2513
+ size: 16,
2514
+ color: "dim"
2515
+ })
2516
+ ]
2517
+ }), /* @__PURE__ */ jsxs(View, {
2518
+ style: styles.changeDetail,
2519
+ children: [/* @__PURE__ */ jsx(Text, {
2520
+ style: [styles.branch, styles.grow],
2521
+ children: branch
2522
+ }), state === "changes" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Text, {
2523
+ style: styles.jade,
2524
+ children: ["+", added]
2525
+ }), /* @__PURE__ */ jsxs(Text, {
2526
+ style: styles.error,
2527
+ children: ["-", removed]
2528
+ })] }) : state === "pushing" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { label: "Pushing..." }), /* @__PURE__ */ jsx(Text, {
2529
+ style: styles.dim,
2530
+ children: "Pushing..."
2531
+ })] }) : /* @__PURE__ */ jsx(Text, {
2532
+ style: styles.dim,
2533
+ children: "No changes yet"
2534
+ })]
2535
+ })]
2536
+ });
2537
+ }
2538
+ function WorktreesActivityView({ git, state, disabled = false, onReview, onWorktrees }) {
2539
+ const { starting } = worktreesPresentation(state);
2540
+ return /* @__PURE__ */ jsxs(View, {
2541
+ style: styles.section,
2542
+ children: [
2543
+ git.changes ? /* @__PURE__ */ jsx(GitChangesRow, {
2544
+ branch: git.changes.branch ?? "Detached HEAD",
2545
+ added: git.changes.added,
2546
+ removed: git.changes.removed,
2547
+ state: git.pending?.startsWith("pushing") ? "pushing" : git.changes.added === 0 && git.changes.removed === 0 && git.changes.files === 0 ? "clean" : "changes",
2548
+ onReview
2549
+ }) : null,
2550
+ /* @__PURE__ */ jsx(WorktreeGroup, {
2551
+ title: "Worktrees",
2552
+ count: `${state.worktrees.length} open${starting ? " · 1 starting" : ""}`
2553
+ }),
2554
+ state.worktrees.map((item) => /* @__PURE__ */ jsx(WorktreeRow, {
2555
+ item,
2556
+ compact: true,
2557
+ onOpen: onWorktrees
2558
+ }, item.id)),
2559
+ starting ? /* @__PURE__ */ jsx(WorktreeStarting, { branch: state.requestedBranch }) : state.pending ? /* @__PURE__ */ jsx(Text, {
2560
+ style: styles.dim,
2561
+ children: state.pending
2562
+ }) : null,
2563
+ state.worktrees.length === 0 && !state.pending ? /* @__PURE__ */ jsxs(View, {
2564
+ style: styles.idle,
2565
+ children: [/* @__PURE__ */ jsx(Text, {
2566
+ style: styles.dim,
2567
+ children: "Idle"
2568
+ }), /* @__PURE__ */ jsx(Button, {
2569
+ testID: "activity-git-create",
2570
+ variant: "ghost",
2571
+ style: styles.link,
2572
+ disabled,
2573
+ onPress: onWorktrees,
2574
+ children: /* @__PURE__ */ jsx(Text, {
2575
+ style: styles.jade,
2576
+ children: "Create a worktree →"
2577
+ })
2578
+ })]
2579
+ }) : null
2580
+ ]
2581
+ });
2582
+ }
2583
+ function WorktreesActivitySection({ sessionId, openTransientTab }) {
2584
+ const git = useGitChanges(sessionId);
2585
+ const state = useWorktrees(sessionId);
2586
+ return /* @__PURE__ */ jsx(WorktreesActivityView, {
2587
+ git,
2588
+ state: {
2589
+ ...state,
2590
+ worktrees: state.worktrees.filter((item) => item.sessionId !== sessionId)
2591
+ },
2592
+ disabled: sessionId === null,
2593
+ onReview: () => openTransientTab(reviewTab()),
2594
+ onWorktrees: () => openTransientTab(worktreesTab())
2595
+ });
2596
+ }
2597
+ const styles = StyleSheet.create((theme) => ({
2598
+ section: {
2599
+ gap: theme.spacing(2),
2600
+ uni__dependencies: [0]
2601
+ },
2602
+ heading: {
2603
+ fontFamily: fontOf(theme.fontFamily.sans),
2604
+ fontSize: theme.fontSize.lg,
2605
+ lineHeight: theme.lineHeight.lg,
2606
+ fontWeight: "600",
2607
+ color: theme.colors.hi,
2608
+ uni__dependencies: [0]
2609
+ },
2610
+ dim: {
2611
+ fontFamily: fontOf(theme.fontFamily.sans),
2612
+ fontSize: theme.fontSize.sm,
2613
+ lineHeight: theme.lineHeight.sm,
2614
+ color: theme.colors.dim,
2615
+ uni__dependencies: [0]
2616
+ },
2617
+ branch: {
2618
+ fontFamily: fontOf(theme.fontFamily.mono),
2619
+ fontSize: theme.fontSize.base,
2620
+ lineHeight: theme.lineHeight.lg,
2621
+ color: theme.colors.hi,
2622
+ uni__dependencies: [0]
2623
+ },
2624
+ line: {
2625
+ flexDirection: "row",
2626
+ alignItems: "center",
2627
+ gap: theme.spacing(2),
2628
+ minHeight: 44,
2629
+ uni__dependencies: [0]
2630
+ },
2631
+ grow: {
2632
+ flex: 1,
2633
+ minWidth: 0,
2634
+ gap: theme.spacing(1),
2635
+ uni__dependencies: [0]
2636
+ },
2637
+ jade: {
2638
+ fontFamily: fontOf(theme.fontFamily.sans),
2639
+ fontSize: theme.fontSize.base,
2640
+ lineHeight: theme.lineHeight.base,
2641
+ color: theme.colors.green,
2642
+ fontWeight: "500",
2643
+ uni__dependencies: [0]
2644
+ },
2645
+ error: {
2646
+ fontFamily: fontOf(theme.fontFamily.sans),
2647
+ fontSize: theme.fontSize.sm,
2648
+ lineHeight: theme.lineHeight.sm,
2649
+ color: theme.colors.red,
2650
+ uni__dependencies: [0]
2651
+ },
2652
+ changeRow: {
2653
+ gap: theme.spacing(1),
2654
+ paddingVertical: theme.spacing(2),
2655
+ borderBottomWidth: StyleSheet.hairlineWidth,
2656
+ borderColor: theme.colors["border-soft"],
2657
+ uni__dependencies: [0]
2658
+ },
2659
+ changeDetail: {
2660
+ flexDirection: "row",
2661
+ alignItems: "center",
2662
+ gap: theme.spacing(3),
2663
+ paddingLeft: theme.spacing(6),
2664
+ minHeight: 28,
2665
+ uni__dependencies: [0]
2666
+ },
2667
+ idle: {
2668
+ flexDirection: "row",
2669
+ alignItems: "center",
2670
+ gap: theme.spacing(4),
2671
+ uni__dependencies: [0]
2672
+ },
2673
+ link: {
2674
+ minHeight: 44,
2675
+ paddingHorizontal: 0
2676
+ }
2677
+ }));
2678
+ //#endregion
2679
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/Worktrees.activity.git.native.tsx
2680
+ var Worktrees_activity_git_native_default = defineFill({ component: WorktreesActivitySection });
2681
+ //#endregion
2682
+ //#region src/extensions/workspaces/sessions/(frontend)/lifecycle/start.native.ts
2683
+ var start_native_default = defineWebLifecycle((runtime) => {
2684
+ const stopWorktrees = startWorktreeRuntime(runtime);
2685
+ const stopChanges = startGitChangesRuntime(runtime);
2686
+ return () => {
2687
+ stopChanges();
2688
+ stopWorktrees();
2689
+ };
2690
+ });
2691
+ //#endregion
2692
+ //#region generated/native.ts
2693
+ const via = (identity, value) => value === void 0 ? void 0 : {
2694
+ ...identity,
2695
+ ...value
2696
+ };
2697
+ const nativePlugin = defineNativePlugin({
2698
+ id: "git",
2699
+ workspace: { get fills() {
2700
+ return [via({
2701
+ slot: "new-session",
2702
+ id: "git-new-session"
2703
+ }, GitNewSession_new_session_native_default)];
2704
+ } },
2705
+ session: {
2706
+ get activityGroups() {
2707
+ return [via({ name: "git" }, git_native_default)];
2708
+ },
2709
+ get channels() {
2710
+ return [via({ channel: "git_changes" }, git_changes_native_default), via({ channel: "git_worktrees" }, git_worktrees_native_default)];
2711
+ },
2712
+ get fills() {
2713
+ return [via({
2714
+ slot: "activity.git",
2715
+ id: "worktrees"
2716
+ }, Worktrees_activity_git_native_default)];
2717
+ },
2718
+ get start() {
2719
+ return start_native_default;
2720
+ }
2721
+ }
2722
+ });
2723
+ //#endregion
2724
+ export { nativePlugin };
2725
+
2726
+ //# sourceMappingURL=native.mjs.map