@agimon-ai/po-git 0.0.0-alpha.0 → 0.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apiContracts.cjs +4 -4
- package/dist/apiContracts.cjs.map +1 -1
- package/dist/apiContracts.mjs +4 -4
- package/dist/apiContracts.mjs.map +1 -1
- package/dist/extensions/native.mjs +1079 -0
- package/dist/extensions/native.mjs.map +1 -0
- package/dist/extensions/server.cjs +13 -13
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts +1 -1
- package/dist/extensions/server.mjs +14 -14
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +121 -135
- package/dist/extensions/web.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{worktreeOperations-CaqwGsD_.mjs → worktreeOperations-BSeyxUai.mjs} +50 -51
- package/dist/worktreeOperations-BSeyxUai.mjs.map +1 -0
- package/dist/{worktreeOperations-MgIVDDu3.cjs → worktreeOperations-DGUbUnYU.cjs} +61 -62
- package/dist/worktreeOperations-DGUbUnYU.cjs.map +1 -0
- package/package.json +35 -17
- package/dist/worktreeOperations-CaqwGsD_.mjs.map +0 -1
- package/dist/worktreeOperations-MgIVDDu3.cjs.map +0 -1
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineGlobalStore, defineNativePlugin, defineSessionStore, defineWebLifecycle, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
|
|
2
|
+
import { BranchIcon, Button, Dot, Input, ListRow, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Textarea, fontOf } from "@agimon-ai/po-native-components";
|
|
3
|
+
import { createElement, useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { View } from "react-native-unistyles/components/native/View";
|
|
5
|
+
import { Text } from "react-native-unistyles/components/native/Text";
|
|
6
|
+
import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
|
|
7
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/types/webWorktrees.ts
|
|
10
|
+
/**
|
|
11
|
+
* The cockpit's view of this session's worktrees.
|
|
12
|
+
*
|
|
13
|
+
* DESIGN PATTERNS:
|
|
14
|
+
* - Declarations only. The channel frame type is a constant so the hub and the
|
|
15
|
+
* page cannot drift apart on a string.
|
|
16
|
+
* - A view is the subset of a record the dock needs, not the record. The
|
|
17
|
+
* registry may hold paths and provenance the page has no business rendering.
|
|
18
|
+
*
|
|
19
|
+
* AVOID:
|
|
20
|
+
* - Adding behaviour here. This file is imported by browser code, so it must
|
|
21
|
+
* stay free of node builtins.
|
|
22
|
+
*/
|
|
23
|
+
/** The data channel carrying one session's worktrees to the page. */
|
|
24
|
+
const GIT_WORKTREES_TYPE = "git_worktrees";
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/worktreesActivityStore.native.ts
|
|
27
|
+
/**
|
|
28
|
+
* The dock's view of this session's worktrees, and the commands it sends.
|
|
29
|
+
*
|
|
30
|
+
* DESIGN PATTERNS:
|
|
31
|
+
* - Session-scoped store fed by one hub channel. The hub is authoritative; the
|
|
32
|
+
* page never reads a registry and never guesses.
|
|
33
|
+
* - Actions are channel frames, not prompts. The hub half owns the tool's
|
|
34
|
+
* operations, so the panel names the work and the hub performs it; a
|
|
35
|
+
* worktree made from the dock is the same object the tool makes, without
|
|
36
|
+
* spending a turn of the conversation to ask for it.
|
|
37
|
+
* - The sender lives in a store rather than a module variable, because a web
|
|
38
|
+
* plugin keeps no mutable module state and the page may reconnect.
|
|
39
|
+
*
|
|
40
|
+
* AVOID:
|
|
41
|
+
* - Calling the hub package API from here. A dock slot gets no sealed request
|
|
42
|
+
* transport, and raw fetch breaks the remote cockpit.
|
|
43
|
+
* - `sendSessionFrame` for a command. It envelopes the frame as a session
|
|
44
|
+
* command bound for the agent, which never reaches a channel's `receive`.
|
|
45
|
+
*/
|
|
46
|
+
const worktreeActivity = defineSessionStore({
|
|
47
|
+
worktrees: [],
|
|
48
|
+
pending: void 0,
|
|
49
|
+
error: void 0,
|
|
50
|
+
errorTarget: void 0
|
|
51
|
+
});
|
|
52
|
+
/** Captures the clicked card, so a focus change cannot retarget an open dialog. */
|
|
53
|
+
const worktreeCreateDialog = defineGlobalStore(null);
|
|
54
|
+
/**
|
|
55
|
+
* The page's hub socket sender, held for as long as the plugin runs.
|
|
56
|
+
*
|
|
57
|
+
* Undefined before `startWorktreeRuntime` and after it disposes; a command
|
|
58
|
+
* sent in that window is dropped rather than queued, because a queue would
|
|
59
|
+
* replay a worktree create against a socket that has since changed sessions.
|
|
60
|
+
*/
|
|
61
|
+
const worktreeSender = defineGlobalStore(void 0);
|
|
62
|
+
/** Binds the page's hub socket. The plugin host calls this once per page. */
|
|
63
|
+
function startWorktreeRuntime(runtime) {
|
|
64
|
+
const send = (frame) => {
|
|
65
|
+
runtime.sendHubFrame(frame);
|
|
66
|
+
};
|
|
67
|
+
worktreeSender.update(() => send);
|
|
68
|
+
return () => {
|
|
69
|
+
worktreeSender.update((current) => current === send ? void 0 : current);
|
|
70
|
+
worktreeCreateDialog.update(() => null);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether this session has worktree work in flight.
|
|
75
|
+
*
|
|
76
|
+
* A standing worktree is state, not work: it sits there until someone closes
|
|
77
|
+
* it, so counting it would leave the session reading as busy forever, with a
|
|
78
|
+
* "background work is still running" notice under a conversation where
|
|
79
|
+
* nothing is running. Only a create or close actually in progress counts.
|
|
80
|
+
*
|
|
81
|
+
* The group stays visible either way. Declaring an active source at all is
|
|
82
|
+
* what keeps it in the dock; this answers how it is drawn, not whether it is
|
|
83
|
+
* there.
|
|
84
|
+
*/
|
|
85
|
+
const worktreeActivitySource = {
|
|
86
|
+
subscribe(listener) {
|
|
87
|
+
const subscription = worktreeActivity.store.subscribe(listener);
|
|
88
|
+
return () => subscription.unsubscribe();
|
|
89
|
+
},
|
|
90
|
+
isActive(sessionId) {
|
|
91
|
+
return worktreeActivity.select(worktreeActivity.store.state, sessionId).pending !== void 0;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
function isRecord$2(value) {
|
|
95
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
function optionalText$1(value) {
|
|
98
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
99
|
+
}
|
|
100
|
+
function optionalErrorTarget$1(value) {
|
|
101
|
+
if (!isRecord$2(value)) return void 0;
|
|
102
|
+
if (value.action === "create") return { action: "create" };
|
|
103
|
+
if (value.action === "close" && typeof value.id === "string" && value.id !== "") return {
|
|
104
|
+
action: "close",
|
|
105
|
+
id: value.id
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** The plugin's session data channel: 'git_worktrees' payloads into the store. */
|
|
109
|
+
const worktreesChannel = worktreeActivity.channel({
|
|
110
|
+
channel: GIT_WORKTREES_TYPE,
|
|
111
|
+
parse(input) {
|
|
112
|
+
if (!isRecord$2(input) || !Array.isArray(input.worktrees)) return null;
|
|
113
|
+
return {
|
|
114
|
+
worktrees: input.worktrees.filter(isRecord$2),
|
|
115
|
+
pending: optionalText$1(input.pending),
|
|
116
|
+
error: optionalText$1(input.error),
|
|
117
|
+
errorTarget: optionalErrorTarget$1(input.errorTarget)
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
reduce(_current, next) {
|
|
121
|
+
return next;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
/** Sends one command to the session's worktrees channel. */
|
|
125
|
+
function command(sessionId, payload) {
|
|
126
|
+
worktreeSender.store.state?.({
|
|
127
|
+
type: GIT_WORKTREES_TYPE,
|
|
128
|
+
sessionId,
|
|
129
|
+
payload
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Asks the hub for a worktree. The hub answers on the same channel. */
|
|
133
|
+
function requestWorktreeCreate(sessionId, branch, baseRef, reservationId) {
|
|
134
|
+
const trimmedBase = baseRef.trim();
|
|
135
|
+
command(sessionId, {
|
|
136
|
+
action: "create",
|
|
137
|
+
branch: branch.trim(),
|
|
138
|
+
...trimmedBase === "" ? {} : { baseRef: trimmedBase },
|
|
139
|
+
...reservationId === void 0 ? {} : { reservationId }
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** Asks the hub to close one worktree. Refusal on a dirty tree stays the hub's call. */
|
|
143
|
+
function requestWorktreeClose(sessionId, id) {
|
|
144
|
+
command(sessionId, {
|
|
145
|
+
action: "close",
|
|
146
|
+
id
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/WorktreesPanel.native.tsx
|
|
151
|
+
function useWorktrees(sessionId) {
|
|
152
|
+
const subscribe = useCallback((listener) => {
|
|
153
|
+
const sub = worktreeActivity.store.subscribe(listener);
|
|
154
|
+
return () => sub.unsubscribe();
|
|
155
|
+
}, []);
|
|
156
|
+
return useSyncExternalStore(subscribe, () => worktreeActivity.select(worktreeActivity.store.state, sessionId));
|
|
157
|
+
}
|
|
158
|
+
function worktreesTab() {
|
|
159
|
+
return {
|
|
160
|
+
id: "git-worktrees",
|
|
161
|
+
label: "worktrees",
|
|
162
|
+
panel: WorktreesPanel
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function WorktreesPanel({ sessionId, sessionReservationId, openSession }) {
|
|
166
|
+
const state = useWorktrees(sessionId);
|
|
167
|
+
const [creating, setCreating] = useState(false);
|
|
168
|
+
const [branch, setBranch] = useState("");
|
|
169
|
+
const [baseRef, setBaseRef] = useState("");
|
|
170
|
+
const [closing, setClosing] = useState();
|
|
171
|
+
const busy = state.pending !== void 0;
|
|
172
|
+
return /* @__PURE__ */ jsxs(ScrollView, {
|
|
173
|
+
contentContainerStyle: styles$1.body,
|
|
174
|
+
children: [
|
|
175
|
+
/* @__PURE__ */ jsx(Button, {
|
|
176
|
+
testID: "git-worktree-create",
|
|
177
|
+
disabled: sessionId === null || busy,
|
|
178
|
+
onPress: () => setCreating(true),
|
|
179
|
+
children: "Create a worktree"
|
|
180
|
+
}),
|
|
181
|
+
state.pending ? /* @__PURE__ */ jsx(Text, {
|
|
182
|
+
style: styles$1.meta,
|
|
183
|
+
children: state.pending
|
|
184
|
+
}) : null,
|
|
185
|
+
state.error ? /* @__PURE__ */ jsx(Text, {
|
|
186
|
+
style: styles$1.error,
|
|
187
|
+
children: state.error
|
|
188
|
+
}) : null,
|
|
189
|
+
state.worktrees.map((worktree) => /* @__PURE__ */ jsxs(View, {
|
|
190
|
+
style: styles$1.card,
|
|
191
|
+
children: [
|
|
192
|
+
/* @__PURE__ */ jsx(ListRow, {
|
|
193
|
+
title: worktree.branch,
|
|
194
|
+
description: `${worktree.path}${worktree.orphaned ? "\nOrphaned" : ""}${worktree.unowned ? "\nUnowned" : ""}`
|
|
195
|
+
}),
|
|
196
|
+
worktree.sessionId && openSession ? /* @__PURE__ */ jsx(Button, {
|
|
197
|
+
variant: "outline",
|
|
198
|
+
onPress: () => openSession(worktree.sessionId),
|
|
199
|
+
children: "Open session"
|
|
200
|
+
}) : null,
|
|
201
|
+
/* @__PURE__ */ jsx(Button, {
|
|
202
|
+
variant: "danger-outline",
|
|
203
|
+
disabled: sessionId === null || busy,
|
|
204
|
+
testID: `git-worktree-close-${worktree.id}`,
|
|
205
|
+
onPress: () => setClosing(worktree.id),
|
|
206
|
+
children: "Close worktree"
|
|
207
|
+
})
|
|
208
|
+
]
|
|
209
|
+
}, worktree.id)),
|
|
210
|
+
state.worktrees.length === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
211
|
+
style: styles$1.meta,
|
|
212
|
+
children: "No worktrees yet. A worktree is its own checkout, branch and session."
|
|
213
|
+
}) : null,
|
|
214
|
+
/* @__PURE__ */ jsx(Sheet, {
|
|
215
|
+
open: creating,
|
|
216
|
+
onOpenChange: setCreating,
|
|
217
|
+
children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "Create a worktree" }) }), /* @__PURE__ */ jsx(SheetBody, { children: /* @__PURE__ */ jsxs(View, {
|
|
218
|
+
style: styles$1.body,
|
|
219
|
+
children: [
|
|
220
|
+
/* @__PURE__ */ jsx(Input, {
|
|
221
|
+
accessibilityLabel: "Branch",
|
|
222
|
+
placeholder: "wt/fix-auth",
|
|
223
|
+
value: branch,
|
|
224
|
+
onChangeText: setBranch
|
|
225
|
+
}),
|
|
226
|
+
/* @__PURE__ */ jsx(Input, {
|
|
227
|
+
accessibilityLabel: "Base ref",
|
|
228
|
+
placeholder: "Base (current)",
|
|
229
|
+
value: baseRef,
|
|
230
|
+
onChangeText: setBaseRef
|
|
231
|
+
}),
|
|
232
|
+
/* @__PURE__ */ jsx(Text, {
|
|
233
|
+
style: styles$1.meta,
|
|
234
|
+
children: "Creates the worktree outside the repository and starts a session nested under this one."
|
|
235
|
+
}),
|
|
236
|
+
/* @__PURE__ */ jsx(Button, {
|
|
237
|
+
disabled: sessionId === null || busy || branch.trim() === "",
|
|
238
|
+
onPress: () => {
|
|
239
|
+
if (sessionId === null) return;
|
|
240
|
+
requestWorktreeCreate(sessionId, branch, baseRef, sessionReservationId);
|
|
241
|
+
setCreating(false);
|
|
242
|
+
if (sessionReservationId === void 0) {
|
|
243
|
+
setBranch("");
|
|
244
|
+
setBaseRef("");
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
children: "Create"
|
|
248
|
+
})
|
|
249
|
+
]
|
|
250
|
+
}) })] })
|
|
251
|
+
}),
|
|
252
|
+
/* @__PURE__ */ jsx(Sheet, {
|
|
253
|
+
open: closing !== void 0,
|
|
254
|
+
onOpenChange: (open) => {
|
|
255
|
+
if (!open) setClosing(void 0);
|
|
256
|
+
},
|
|
257
|
+
children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "Close this worktree?" }) }), /* @__PURE__ */ jsxs(SheetBody, { children: [/* @__PURE__ */ jsx(Text, {
|
|
258
|
+
style: styles$1.meta,
|
|
259
|
+
children: "The checkout is removed. Git refuses a dirty worktree."
|
|
260
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
261
|
+
variant: "danger-outline",
|
|
262
|
+
disabled: busy,
|
|
263
|
+
onPress: () => {
|
|
264
|
+
if (sessionId !== null && closing !== void 0) requestWorktreeClose(sessionId, closing);
|
|
265
|
+
setClosing(void 0);
|
|
266
|
+
},
|
|
267
|
+
children: "Close worktree"
|
|
268
|
+
})] })] })
|
|
269
|
+
})
|
|
270
|
+
]
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const styles$1 = StyleSheet.create((theme) => ({
|
|
274
|
+
body: {
|
|
275
|
+
padding: theme.spacing(3),
|
|
276
|
+
gap: theme.spacing(2),
|
|
277
|
+
uni__dependencies: [0]
|
|
278
|
+
},
|
|
279
|
+
card: {
|
|
280
|
+
borderWidth: 1,
|
|
281
|
+
borderColor: theme.colors["border-soft"],
|
|
282
|
+
borderRadius: theme.borderRadius.md,
|
|
283
|
+
backgroundColor: theme.colors.panel,
|
|
284
|
+
padding: theme.spacing(2),
|
|
285
|
+
gap: theme.spacing(1),
|
|
286
|
+
uni__dependencies: [0]
|
|
287
|
+
},
|
|
288
|
+
meta: {
|
|
289
|
+
color: theme.colors.dim,
|
|
290
|
+
fontSize: theme.fontSize.sm,
|
|
291
|
+
uni__dependencies: [0]
|
|
292
|
+
},
|
|
293
|
+
error: {
|
|
294
|
+
color: theme.colors.red,
|
|
295
|
+
fontSize: theme.fontSize.sm,
|
|
296
|
+
uni__dependencies: [0]
|
|
297
|
+
}
|
|
298
|
+
}));
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/types/gitReview.ts
|
|
301
|
+
/**
|
|
302
|
+
* The cockpit's view of what this session changed against its base: the counts
|
|
303
|
+
* the '# diff' group shows, what the review tab lists and draws, and the manual
|
|
304
|
+
* sync that sits beside them.
|
|
305
|
+
*
|
|
306
|
+
* DESIGN PATTERNS:
|
|
307
|
+
* - Declarations only, shared by the hub and the page.
|
|
308
|
+
* - Its own channel, apart from worktrees. Worktrees are repository
|
|
309
|
+
* management; the diff is this session's code change.
|
|
310
|
+
* - A diff row carries one line number whose file follows from its marker: the
|
|
311
|
+
* new file's for context and additions, the old file's for removals. It is
|
|
312
|
+
* the same shape the shared DiffView draws.
|
|
313
|
+
*
|
|
314
|
+
* AVOID:
|
|
315
|
+
* - Behaviour or node builtins. Browser code imports this file.
|
|
316
|
+
*/
|
|
317
|
+
/** The session channel carrying this checkout's changes and the sync around them. */
|
|
318
|
+
const GIT_CHANGES_TYPE = "git_changes";
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/gitChangesStore.native.ts
|
|
321
|
+
/**
|
|
322
|
+
* This session's code change against its base, and the manual sync beside it.
|
|
323
|
+
*
|
|
324
|
+
* DESIGN PATTERNS:
|
|
325
|
+
* - Its own session store and hub channel, apart from worktrees. Worktrees are
|
|
326
|
+
* repository management; this is the session's diff.
|
|
327
|
+
* - The hub is authoritative: it computes the counts, runs pull, push and
|
|
328
|
+
* rebase one at a time, and reports pending, errors and a paused rebase here.
|
|
329
|
+
* - The sender lives in a store rather than a module variable, because a web
|
|
330
|
+
* plugin keeps no mutable module state and the page may reconnect.
|
|
331
|
+
*
|
|
332
|
+
* AVOID:
|
|
333
|
+
* - `sendSessionFrame` for a sync command. That frame is bound for the agent
|
|
334
|
+
* and never reaches a channel's `receive`.
|
|
335
|
+
*/
|
|
336
|
+
const gitChanges = defineSessionStore({
|
|
337
|
+
changes: void 0,
|
|
338
|
+
pending: void 0,
|
|
339
|
+
error: void 0,
|
|
340
|
+
errorTarget: void 0
|
|
341
|
+
});
|
|
342
|
+
/** The page's hub socket sender; undefined before start and after dispose, when a command is dropped. */
|
|
343
|
+
const changesSender = defineGlobalStore(void 0);
|
|
344
|
+
/** Binds the page's hub socket for sync commands. The plugin host calls this once per page. */
|
|
345
|
+
function startGitChangesRuntime(runtime) {
|
|
346
|
+
const send = (frame) => {
|
|
347
|
+
runtime.sendHubFrame(frame);
|
|
348
|
+
};
|
|
349
|
+
changesSender.update(() => send);
|
|
350
|
+
return () => {
|
|
351
|
+
changesSender.update((current) => current === send ? void 0 : current);
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/** Whether a sync command is running for this session. */
|
|
355
|
+
const gitChangesActivitySource = {
|
|
356
|
+
subscribe(listener) {
|
|
357
|
+
const subscription = gitChanges.store.subscribe(listener);
|
|
358
|
+
return () => subscription.unsubscribe();
|
|
359
|
+
},
|
|
360
|
+
isActive(sessionId) {
|
|
361
|
+
return gitChanges.select(gitChanges.store.state, sessionId).pending !== void 0;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
function isRecord$1(value) {
|
|
365
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
366
|
+
}
|
|
367
|
+
function optionalText(value) {
|
|
368
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
369
|
+
}
|
|
370
|
+
function count(value) {
|
|
371
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
372
|
+
}
|
|
373
|
+
/** The page never trusts a frame's shape: a malformed field drops, the rest still renders. */
|
|
374
|
+
const MAX_CONFLICTS = 100;
|
|
375
|
+
function optionalChanges(value) {
|
|
376
|
+
if (!isRecord$1(value)) return void 0;
|
|
377
|
+
const added = count(value.added);
|
|
378
|
+
const removed = count(value.removed);
|
|
379
|
+
const files = count(value.files);
|
|
380
|
+
if (added === void 0 || removed === void 0 || files === void 0) return void 0;
|
|
381
|
+
const branch = optionalText(value.branch);
|
|
382
|
+
const base = optionalText(value.base);
|
|
383
|
+
const upstream = isRecord$1(value.upstream) ? value.upstream : void 0;
|
|
384
|
+
const ahead = count(upstream?.ahead);
|
|
385
|
+
const behind = count(upstream?.behind);
|
|
386
|
+
const upstreamRef = optionalText(upstream?.ref);
|
|
387
|
+
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;
|
|
388
|
+
return {
|
|
389
|
+
added,
|
|
390
|
+
removed,
|
|
391
|
+
files,
|
|
392
|
+
...branch === void 0 ? {} : { branch },
|
|
393
|
+
...base === void 0 ? {} : { base },
|
|
394
|
+
...value.truncated === true ? { truncated: true } : {},
|
|
395
|
+
...upstreamRef === void 0 || ahead === void 0 || behind === void 0 ? {} : { upstream: {
|
|
396
|
+
ref: upstreamRef,
|
|
397
|
+
ahead,
|
|
398
|
+
behind
|
|
399
|
+
} },
|
|
400
|
+
...conflicts === void 0 ? {} : { rebase: { conflicts } }
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function optionalErrorTarget(value) {
|
|
404
|
+
if (!isRecord$1(value)) return void 0;
|
|
405
|
+
if (value.action === "pull" || value.action === "rebase" || value.action === "abort-rebase") return { action: value.action };
|
|
406
|
+
if (value.action === "push") return value.forceRequired === true ? {
|
|
407
|
+
action: "push",
|
|
408
|
+
forceRequired: true
|
|
409
|
+
} : { action: "push" };
|
|
410
|
+
}
|
|
411
|
+
/** The session data channel: 'git_changes' payloads into the store. */
|
|
412
|
+
const gitChangesChannel = gitChanges.channel({
|
|
413
|
+
channel: GIT_CHANGES_TYPE,
|
|
414
|
+
parse(input) {
|
|
415
|
+
if (!isRecord$1(input)) return null;
|
|
416
|
+
return {
|
|
417
|
+
changes: optionalChanges(input.changes),
|
|
418
|
+
pending: optionalText(input.pending),
|
|
419
|
+
error: optionalText(input.error),
|
|
420
|
+
errorTarget: optionalErrorTarget(input.errorTarget)
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
reduce(_current, next) {
|
|
424
|
+
return next;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
/**
|
|
428
|
+
* Asks the hub to pull, push, rebase or abort a paused rebase. `force` is only
|
|
429
|
+
* ever sent after the reader confirmed a lease force for a rejected push.
|
|
430
|
+
*/
|
|
431
|
+
function requestGitSync(sessionId, action, force = false) {
|
|
432
|
+
const payload = action === "push" && force ? {
|
|
433
|
+
action: "push",
|
|
434
|
+
force: true
|
|
435
|
+
} : { action };
|
|
436
|
+
changesSender.store.state?.({
|
|
437
|
+
type: GIT_CHANGES_TYPE,
|
|
438
|
+
sessionId,
|
|
439
|
+
payload
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/extensions/workspaces/sessions/(frontend)/activity-group/git.native.ts
|
|
444
|
+
var git_native_default = defineActivityGroup({
|
|
445
|
+
keys: "g w",
|
|
446
|
+
icon: () => createElement(BranchIcon),
|
|
447
|
+
activeSource: {
|
|
448
|
+
subscribe(listener) {
|
|
449
|
+
const stopWorktrees = worktreeActivitySource.subscribe(listener);
|
|
450
|
+
const stopChanges = gitChangesActivitySource.subscribe(listener);
|
|
451
|
+
return () => {
|
|
452
|
+
stopWorktrees();
|
|
453
|
+
stopChanges();
|
|
454
|
+
};
|
|
455
|
+
},
|
|
456
|
+
isActive(sessionId) {
|
|
457
|
+
return worktreeActivitySource.isActive(sessionId) || gitChangesActivitySource.isActive(sessionId);
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
marksBackgroundWork: false,
|
|
461
|
+
transientTab: worktreesTab,
|
|
462
|
+
order: 30
|
|
463
|
+
});
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/git-changes.native.ts
|
|
466
|
+
var git_changes_native_default = defineChannelFile(gitChangesChannel);
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/git-worktrees.native.ts
|
|
469
|
+
var git_worktrees_native_default = defineChannelFile(worktreesChannel);
|
|
470
|
+
const api = createApiClient({
|
|
471
|
+
/** Workspace: the new-session dialog's branch list. */
|
|
472
|
+
branches: {
|
|
473
|
+
method: "GET",
|
|
474
|
+
path: "/branches",
|
|
475
|
+
response: void 0
|
|
476
|
+
},
|
|
477
|
+
/** Workspace: a top-level worktree session on an existing or new branch. */
|
|
478
|
+
createSession: {
|
|
479
|
+
method: "POST",
|
|
480
|
+
path: "/sessions",
|
|
481
|
+
response: void 0
|
|
482
|
+
},
|
|
483
|
+
/** Workspace: that workspace's saved remote auth, never the token itself. */
|
|
484
|
+
auth: {
|
|
485
|
+
method: "GET",
|
|
486
|
+
path: "/auth",
|
|
487
|
+
query: ["repositoryId"],
|
|
488
|
+
response: void 0
|
|
489
|
+
},
|
|
490
|
+
/** Workspace: replaces that workspace's remote auth. Passkey step-up gated for remote devices. */
|
|
491
|
+
saveAuth: {
|
|
492
|
+
method: "PUT",
|
|
493
|
+
path: "/auth",
|
|
494
|
+
query: ["repositoryId"],
|
|
495
|
+
response: void 0
|
|
496
|
+
},
|
|
497
|
+
/** Session: the files this session's checkout changed against its base. */
|
|
498
|
+
review: {
|
|
499
|
+
method: "GET",
|
|
500
|
+
path: "/review",
|
|
501
|
+
response: void 0
|
|
502
|
+
},
|
|
503
|
+
/** Session: one changed file's hunks. */
|
|
504
|
+
reviewFile: {
|
|
505
|
+
method: "GET",
|
|
506
|
+
path: "/review/file",
|
|
507
|
+
query: ["path"],
|
|
508
|
+
response: void 0
|
|
509
|
+
}
|
|
510
|
+
}, {
|
|
511
|
+
scopes: [
|
|
512
|
+
"global",
|
|
513
|
+
"workspace",
|
|
514
|
+
"session"
|
|
515
|
+
],
|
|
516
|
+
basePath: "git",
|
|
517
|
+
transport: nativeApiTransport,
|
|
518
|
+
sessionAddress: sessionApiAddress
|
|
519
|
+
});
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewApi.native.ts
|
|
522
|
+
/**
|
|
523
|
+
* The review tab's half of the session API: the changed-file list and one
|
|
524
|
+
* file's hunks.
|
|
525
|
+
*
|
|
526
|
+
* The generated client owns the URL and the sealed transport, so nothing here
|
|
527
|
+
* spells a route or reaches for `fetch`. What travels is file contents, which a
|
|
528
|
+
* tunnel's relay must never see in the clear.
|
|
529
|
+
*/
|
|
530
|
+
const UNREACHABLE = "The session is unreachable.";
|
|
531
|
+
function isRecord(value) {
|
|
532
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
533
|
+
}
|
|
534
|
+
/** The message a reader is shown: the route's own words, or why there were none. */
|
|
535
|
+
function messageOf(result) {
|
|
536
|
+
if (result.status === 0) return UNREACHABLE;
|
|
537
|
+
return result.error === "" ? `The session answered ${String(result.status)}.` : result.error;
|
|
538
|
+
}
|
|
539
|
+
async function fetchReviewSummary(sessionId, signal) {
|
|
540
|
+
const result = await api.session(sessionId).review(signal === void 0 ? {} : { signal });
|
|
541
|
+
if (!result.ok) return {
|
|
542
|
+
ok: false,
|
|
543
|
+
error: messageOf(result)
|
|
544
|
+
};
|
|
545
|
+
if (!isRecord(result.data) || !Array.isArray(result.data.files)) return {
|
|
546
|
+
ok: false,
|
|
547
|
+
error: "The session answered with no file list."
|
|
548
|
+
};
|
|
549
|
+
return {
|
|
550
|
+
ok: true,
|
|
551
|
+
summary: result.data
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
async function fetchReviewFile(sessionId, path, signal) {
|
|
555
|
+
const result = await api.session(sessionId).reviewFile({
|
|
556
|
+
query: { path },
|
|
557
|
+
...signal === void 0 ? {} : { signal }
|
|
558
|
+
});
|
|
559
|
+
if (!result.ok) return {
|
|
560
|
+
ok: false,
|
|
561
|
+
error: messageOf(result)
|
|
562
|
+
};
|
|
563
|
+
if (!isRecord(result.data) || !Array.isArray(result.data.hunks)) return {
|
|
564
|
+
ok: false,
|
|
565
|
+
error: "The session answered with no diff."
|
|
566
|
+
};
|
|
567
|
+
return {
|
|
568
|
+
ok: true,
|
|
569
|
+
diff: result.data
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewStore.native.ts
|
|
574
|
+
const gitReview = defineSessionStore({ comments: [] });
|
|
575
|
+
function addReviewComment(sessionId, comment) {
|
|
576
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
577
|
+
gitReview.update(sessionId, (current) => ({ comments: [...current.comments, {
|
|
578
|
+
...comment,
|
|
579
|
+
id
|
|
580
|
+
}] }));
|
|
581
|
+
}
|
|
582
|
+
function removeReviewComment(sessionId, id) {
|
|
583
|
+
gitReview.update(sessionId, (current) => ({ comments: current.comments.filter((comment) => comment.id !== id) }));
|
|
584
|
+
}
|
|
585
|
+
function clearReviewComments(sessionId) {
|
|
586
|
+
gitReview.update(sessionId, () => ({ comments: [] }));
|
|
587
|
+
}
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewView.ts
|
|
590
|
+
/**
|
|
591
|
+
* Pure wording for what the review tab sends the agent, and the one send.
|
|
592
|
+
*
|
|
593
|
+
* Both messages go as `follow_up`: the host starts a turn when idle, or after
|
|
594
|
+
* the current one, without treating a review send as an explicit Queue action.
|
|
595
|
+
* A `prompt` frame instead fails while a turn runs.
|
|
596
|
+
*/
|
|
597
|
+
/** The opening line of a review, naming what was reviewed. */
|
|
598
|
+
function reviewHeading(count, branch, base) {
|
|
599
|
+
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."}`;
|
|
600
|
+
}
|
|
601
|
+
/** What "ask agent to resolve" sends: the files, and the steps that finish a paused rebase. */
|
|
602
|
+
function conflictPrompt(branch, conflicts) {
|
|
603
|
+
const files = conflicts.length === 0 ? "- (git reports no unmerged paths yet)" : conflicts.map((file) => `- ${file}`).join("\n");
|
|
604
|
+
return [
|
|
605
|
+
`A rebase of ${branch ?? "this checkout"} stopped on conflicts in:`,
|
|
606
|
+
"",
|
|
607
|
+
files,
|
|
608
|
+
"",
|
|
609
|
+
"Please resolve them:",
|
|
610
|
+
"1. Fix the conflict markers in each file, keeping the intent of both sides.",
|
|
611
|
+
"2. `git add` each resolved file.",
|
|
612
|
+
"3. Run `GIT_EDITOR=true git rebase --continue`, and repeat if it stops again.",
|
|
613
|
+
"",
|
|
614
|
+
"Do not push. I will push from the cockpit once the rebase is done."
|
|
615
|
+
].join("\n");
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Hands one deferred follow-up to the session. Returns the reason it failed,
|
|
619
|
+
* or undefined once sent. The sender throws when the session is disconnected.
|
|
620
|
+
*/
|
|
621
|
+
function deliverToAgent(send, sessionId, message) {
|
|
622
|
+
try {
|
|
623
|
+
send(sessionId, {
|
|
624
|
+
type: "follow_up",
|
|
625
|
+
message
|
|
626
|
+
});
|
|
627
|
+
return;
|
|
628
|
+
} catch (error) {
|
|
629
|
+
return error instanceof Error ? error.message : "The session did not take the message.";
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/GitReviewPanel.native.tsx
|
|
634
|
+
function useGitChanges(sessionId) {
|
|
635
|
+
const subscribe = useCallback((listener) => {
|
|
636
|
+
const sub = gitChanges.store.subscribe(listener);
|
|
637
|
+
return () => sub.unsubscribe();
|
|
638
|
+
}, []);
|
|
639
|
+
return useSyncExternalStore(subscribe, () => gitChanges.select(gitChanges.store.state, sessionId));
|
|
640
|
+
}
|
|
641
|
+
function reviewTab() {
|
|
642
|
+
return {
|
|
643
|
+
id: "git-review",
|
|
644
|
+
label: "review",
|
|
645
|
+
panel: GitReviewPanel
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function GitReviewPanel({ sessionId, sendSessionFrame }) {
|
|
649
|
+
const activity = useGitChanges(sessionId);
|
|
650
|
+
const subscribe = useCallback((listener) => {
|
|
651
|
+
const sub = gitReview.store.subscribe(listener);
|
|
652
|
+
return () => sub.unsubscribe();
|
|
653
|
+
}, []);
|
|
654
|
+
const comments = useSyncExternalStore(subscribe, () => gitReview.select(gitReview.store.state, sessionId).comments);
|
|
655
|
+
const [summary, setSummary] = useState();
|
|
656
|
+
const [file, setFile] = useState();
|
|
657
|
+
const [error, setError] = useState();
|
|
658
|
+
const [dismissedError, setDismissedError] = useState();
|
|
659
|
+
const [confirm, setConfirm] = useState();
|
|
660
|
+
const [draft, setDraft] = useState();
|
|
661
|
+
const [comment, setComment] = useState("");
|
|
662
|
+
const [start, setStart] = useState("");
|
|
663
|
+
const [end, setEnd] = useState("");
|
|
664
|
+
const request = useRef(void 0);
|
|
665
|
+
const fingerprint = JSON.stringify(activity.changes);
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
if (sessionId === null) return void 0;
|
|
668
|
+
const controller = new AbortController();
|
|
669
|
+
fetchReviewSummary(sessionId, controller.signal).then((result) => {
|
|
670
|
+
if (!controller.signal.aborted) {
|
|
671
|
+
if (result.ok) setSummary(result.summary);
|
|
672
|
+
else setError(result.error);
|
|
673
|
+
}
|
|
674
|
+
}).catch((cause) => {
|
|
675
|
+
if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
|
|
676
|
+
});
|
|
677
|
+
return () => {
|
|
678
|
+
controller.abort();
|
|
679
|
+
request.current?.abort();
|
|
680
|
+
};
|
|
681
|
+
}, [sessionId, fingerprint]);
|
|
682
|
+
const loadFile = async (path) => {
|
|
683
|
+
if (sessionId === null) return;
|
|
684
|
+
request.current?.abort();
|
|
685
|
+
const controller = new AbortController();
|
|
686
|
+
request.current = controller;
|
|
687
|
+
setFile(void 0);
|
|
688
|
+
setError(void 0);
|
|
689
|
+
try {
|
|
690
|
+
const result = await fetchReviewFile(sessionId, path, controller.signal);
|
|
691
|
+
if (!controller.signal.aborted) {
|
|
692
|
+
if (result.ok) setFile(result.diff);
|
|
693
|
+
else setError(result.error);
|
|
694
|
+
}
|
|
695
|
+
} catch (cause) {
|
|
696
|
+
if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
if (sessionId === null) return null;
|
|
700
|
+
const changes = activity.changes;
|
|
701
|
+
const busy = activity.pending !== void 0;
|
|
702
|
+
const paused = changes?.rebase !== void 0;
|
|
703
|
+
const send = (message, onSent) => {
|
|
704
|
+
const failure = deliverToAgent(sendSessionFrame, sessionId, message);
|
|
705
|
+
setError(failure);
|
|
706
|
+
if (failure === void 0) onSent?.();
|
|
707
|
+
};
|
|
708
|
+
const sync = (action) => requestGitSync(sessionId, action);
|
|
709
|
+
const forceRequired = activity.error !== dismissedError && activity.errorTarget?.action === "push" && activity.errorTarget.forceRequired === true;
|
|
710
|
+
return /* @__PURE__ */ jsxs(ScrollView, {
|
|
711
|
+
contentContainerStyle: styles.body,
|
|
712
|
+
children: [
|
|
713
|
+
/* @__PURE__ */ jsx(Text, {
|
|
714
|
+
style: styles.title,
|
|
715
|
+
children: changes?.branch ?? "Detached HEAD"
|
|
716
|
+
}),
|
|
717
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
718
|
+
style: styles.meta,
|
|
719
|
+
children: [
|
|
720
|
+
changes?.base ? `vs ${changes.base}` : "Uncommitted changes",
|
|
721
|
+
" · +",
|
|
722
|
+
changes?.added ?? 0,
|
|
723
|
+
" -",
|
|
724
|
+
changes?.removed ?? 0,
|
|
725
|
+
"\\n",
|
|
726
|
+
changes?.upstream ? `↑${changes.upstream.ahead} ↓${changes.upstream.behind} ${changes.upstream.ref}` : "No upstream yet"
|
|
727
|
+
]
|
|
728
|
+
}),
|
|
729
|
+
/* @__PURE__ */ jsxs(View, {
|
|
730
|
+
style: styles.actions,
|
|
731
|
+
children: [
|
|
732
|
+
/* @__PURE__ */ jsx(Button, {
|
|
733
|
+
variant: "outline",
|
|
734
|
+
disabled: busy || paused || changes?.upstream === void 0,
|
|
735
|
+
onPress: () => sync("pull"),
|
|
736
|
+
children: "Pull"
|
|
737
|
+
}),
|
|
738
|
+
/* @__PURE__ */ jsx(Button, {
|
|
739
|
+
variant: "outline",
|
|
740
|
+
disabled: busy || paused || changes?.branch === void 0,
|
|
741
|
+
onPress: () => sync("push"),
|
|
742
|
+
children: "Push"
|
|
743
|
+
}),
|
|
744
|
+
/* @__PURE__ */ jsx(Button, {
|
|
745
|
+
variant: "outline",
|
|
746
|
+
disabled: busy || paused || changes?.base === void 0,
|
|
747
|
+
onPress: () => sync("rebase"),
|
|
748
|
+
children: "Rebase"
|
|
749
|
+
})
|
|
750
|
+
]
|
|
751
|
+
}),
|
|
752
|
+
activity.pending ? /* @__PURE__ */ jsx(Text, {
|
|
753
|
+
style: styles.meta,
|
|
754
|
+
children: activity.pending
|
|
755
|
+
}) : null,
|
|
756
|
+
activity.error && activity.error !== dismissedError ? /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsx(Text, {
|
|
757
|
+
style: styles.error,
|
|
758
|
+
children: activity.error
|
|
759
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
760
|
+
variant: "ghost",
|
|
761
|
+
onPress: () => setDismissedError(activity.error),
|
|
762
|
+
children: "Dismiss"
|
|
763
|
+
})] }) : null,
|
|
764
|
+
forceRequired ? /* @__PURE__ */ jsx(Button, {
|
|
765
|
+
variant: "danger-outline",
|
|
766
|
+
disabled: busy,
|
|
767
|
+
onPress: () => setConfirm("force"),
|
|
768
|
+
children: "Force push with lease…"
|
|
769
|
+
}) : null,
|
|
770
|
+
paused ? /* @__PURE__ */ jsxs(View, {
|
|
771
|
+
style: styles.body,
|
|
772
|
+
children: [
|
|
773
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
774
|
+
style: styles.error,
|
|
775
|
+
children: ["Rebase paused\\n", changes.rebase?.conflicts.join("\n")]
|
|
776
|
+
}),
|
|
777
|
+
/* @__PURE__ */ jsx(Button, {
|
|
778
|
+
disabled: busy,
|
|
779
|
+
onPress: () => send(conflictPrompt(changes.branch, changes.rebase?.conflicts ?? [])),
|
|
780
|
+
children: "Ask agent to resolve"
|
|
781
|
+
}),
|
|
782
|
+
/* @__PURE__ */ jsx(Button, {
|
|
783
|
+
variant: "danger-outline",
|
|
784
|
+
disabled: busy,
|
|
785
|
+
onPress: () => setConfirm("abort"),
|
|
786
|
+
children: "Abort rebase…"
|
|
787
|
+
})
|
|
788
|
+
]
|
|
789
|
+
}) : null,
|
|
790
|
+
error ? /* @__PURE__ */ jsx(Text, {
|
|
791
|
+
style: styles.error,
|
|
792
|
+
children: error
|
|
793
|
+
}) : null,
|
|
794
|
+
summary ? summary.files.map((entry) => /* @__PURE__ */ jsxs(Button, {
|
|
795
|
+
variant: "ghost",
|
|
796
|
+
testID: `git-review-file-${entry.path}`,
|
|
797
|
+
onPress: () => void loadFile(entry.path),
|
|
798
|
+
children: [
|
|
799
|
+
entry.path,
|
|
800
|
+
" · ",
|
|
801
|
+
entry.status,
|
|
802
|
+
" · +",
|
|
803
|
+
entry.added,
|
|
804
|
+
" -",
|
|
805
|
+
entry.removed
|
|
806
|
+
]
|
|
807
|
+
}, entry.path)) : /* @__PURE__ */ jsx(Text, {
|
|
808
|
+
style: styles.meta,
|
|
809
|
+
children: "Loading changes…"
|
|
810
|
+
}),
|
|
811
|
+
summary?.files.length === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
812
|
+
style: styles.meta,
|
|
813
|
+
children: summary.repository ? "No changed files" : "Not a Git checkout"
|
|
814
|
+
}) : null,
|
|
815
|
+
summary?.truncated ? /* @__PURE__ */ jsx(Text, {
|
|
816
|
+
style: styles.meta,
|
|
817
|
+
children: "The file list was truncated."
|
|
818
|
+
}) : null,
|
|
819
|
+
file ? /* @__PURE__ */ jsxs(View, {
|
|
820
|
+
style: styles.body,
|
|
821
|
+
children: [
|
|
822
|
+
/* @__PURE__ */ jsx(Text, {
|
|
823
|
+
style: styles.title,
|
|
824
|
+
children: file.path
|
|
825
|
+
}),
|
|
826
|
+
file.binary || file.tooLarge ? /* @__PURE__ */ jsx(Text, {
|
|
827
|
+
style: styles.meta,
|
|
828
|
+
children: file.binary ? "Binary file" : "Diff too large to display"
|
|
829
|
+
}) : file.hunks.map((hunk, index) => /* @__PURE__ */ jsx(View, { children: hunk.rows.map((row, rowIndex) => /* @__PURE__ */ jsxs(Text, {
|
|
830
|
+
selectable: true,
|
|
831
|
+
style: styles.diff(row.marker),
|
|
832
|
+
onPress: () => {
|
|
833
|
+
setDraft({
|
|
834
|
+
path: file.path,
|
|
835
|
+
rows: hunk.rows
|
|
836
|
+
});
|
|
837
|
+
setStart(String(row.line));
|
|
838
|
+
setEnd(String(row.line));
|
|
839
|
+
},
|
|
840
|
+
children: [
|
|
841
|
+
row.marker,
|
|
842
|
+
" ",
|
|
843
|
+
row.line,
|
|
844
|
+
" ",
|
|
845
|
+
row.content
|
|
846
|
+
]
|
|
847
|
+
}, rowIndex)) }, index)),
|
|
848
|
+
/* @__PURE__ */ jsx(Button, {
|
|
849
|
+
variant: "outline",
|
|
850
|
+
onPress: () => {
|
|
851
|
+
setDraft({
|
|
852
|
+
path: file.path,
|
|
853
|
+
rows: []
|
|
854
|
+
});
|
|
855
|
+
setStart("");
|
|
856
|
+
setEnd("");
|
|
857
|
+
},
|
|
858
|
+
children: "Comment on file"
|
|
859
|
+
})
|
|
860
|
+
]
|
|
861
|
+
}) : null,
|
|
862
|
+
comments.map((item) => /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Text, {
|
|
863
|
+
style: styles.meta,
|
|
864
|
+
children: [
|
|
865
|
+
item.relPath,
|
|
866
|
+
item.startLine === void 0 ? "" : `:${item.startLine}-${item.endLine ?? item.startLine}`,
|
|
867
|
+
"\\n",
|
|
868
|
+
item.body
|
|
869
|
+
]
|
|
870
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
871
|
+
variant: "ghost",
|
|
872
|
+
onPress: () => removeReviewComment(sessionId, item.id),
|
|
873
|
+
children: "Remove comment"
|
|
874
|
+
})] }, item.id)),
|
|
875
|
+
comments.length > 0 ? /* @__PURE__ */ jsxs(View, {
|
|
876
|
+
style: styles.actions,
|
|
877
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
878
|
+
onPress: () => {
|
|
879
|
+
const body = comments.map((item) => {
|
|
880
|
+
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()}`;
|
|
881
|
+
}).join("\n\n");
|
|
882
|
+
send(`${reviewHeading(comments.length, summary?.branch, summary?.base)}\n\n${body}`, () => clearReviewComments(sessionId));
|
|
883
|
+
},
|
|
884
|
+
children: "Send review"
|
|
885
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
886
|
+
variant: "outline",
|
|
887
|
+
onPress: () => setConfirm("discard"),
|
|
888
|
+
children: "Discard review…"
|
|
889
|
+
})]
|
|
890
|
+
}) : null,
|
|
891
|
+
/* @__PURE__ */ jsx(Sheet, {
|
|
892
|
+
open: draft !== void 0,
|
|
893
|
+
onOpenChange: (open) => {
|
|
894
|
+
if (!open) setDraft(void 0);
|
|
895
|
+
},
|
|
896
|
+
children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "Review comment" }) }), /* @__PURE__ */ jsx(SheetBody, { children: /* @__PURE__ */ jsxs(View, {
|
|
897
|
+
style: styles.body,
|
|
898
|
+
children: [
|
|
899
|
+
draft?.rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Input, {
|
|
900
|
+
accessibilityLabel: "Start line",
|
|
901
|
+
value: start,
|
|
902
|
+
onChangeText: setStart,
|
|
903
|
+
keyboardType: "number-pad"
|
|
904
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
905
|
+
accessibilityLabel: "End line",
|
|
906
|
+
value: end,
|
|
907
|
+
onChangeText: setEnd,
|
|
908
|
+
keyboardType: "number-pad"
|
|
909
|
+
})] }) : null,
|
|
910
|
+
/* @__PURE__ */ jsx(Textarea, {
|
|
911
|
+
accessibilityLabel: "Comment",
|
|
912
|
+
value: comment,
|
|
913
|
+
onChangeText: setComment
|
|
914
|
+
}),
|
|
915
|
+
/* @__PURE__ */ jsx(Button, {
|
|
916
|
+
disabled: comment.trim() === "" || Boolean(draft?.rows.length) && (!Number.isInteger(Number(start)) || Number(start) < 1 || !Number.isInteger(Number(end)) || Number(end) < Number(start)),
|
|
917
|
+
onPress: () => {
|
|
918
|
+
if (!draft) return;
|
|
919
|
+
const rows = draft.rows.filter((row) => row.line >= Number(start) && row.line <= Number(end));
|
|
920
|
+
const kept = rows.filter((row) => row.marker !== "-");
|
|
921
|
+
const numbered = kept.length > 0 ? kept : rows;
|
|
922
|
+
addReviewComment(sessionId, {
|
|
923
|
+
path: draft.path,
|
|
924
|
+
relPath: draft.path,
|
|
925
|
+
body: comment.trim(),
|
|
926
|
+
snippet: numbered.map((row) => row.content).join("\n"),
|
|
927
|
+
...numbered.length === 0 ? {} : {
|
|
928
|
+
side: kept.length > 0 ? "new" : "old",
|
|
929
|
+
startLine: Math.min(...numbered.map((row) => row.line)),
|
|
930
|
+
endLine: Math.max(...numbered.map((row) => row.line))
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
setDraft(void 0);
|
|
934
|
+
setComment("");
|
|
935
|
+
},
|
|
936
|
+
children: "Add comment"
|
|
937
|
+
})
|
|
938
|
+
]
|
|
939
|
+
}) })] })
|
|
940
|
+
}),
|
|
941
|
+
/* @__PURE__ */ jsx(Sheet, {
|
|
942
|
+
open: confirm !== void 0,
|
|
943
|
+
onOpenChange: (open) => {
|
|
944
|
+
if (!open) setConfirm(void 0);
|
|
945
|
+
},
|
|
946
|
+
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: [/* @__PURE__ */ jsx(Text, {
|
|
947
|
+
style: styles.meta,
|
|
948
|
+
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."
|
|
949
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
950
|
+
variant: "danger-outline",
|
|
951
|
+
onPress: () => {
|
|
952
|
+
if (confirm === "force") requestGitSync(sessionId, "push", true);
|
|
953
|
+
else if (confirm === "abort") sync("abort-rebase");
|
|
954
|
+
else clearReviewComments(sessionId);
|
|
955
|
+
setConfirm(void 0);
|
|
956
|
+
},
|
|
957
|
+
children: "Confirm"
|
|
958
|
+
})] })] })
|
|
959
|
+
})
|
|
960
|
+
]
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
const styles = StyleSheet.create((theme) => ({
|
|
964
|
+
body: {
|
|
965
|
+
padding: theme.spacing(3),
|
|
966
|
+
gap: theme.spacing(2),
|
|
967
|
+
uni__dependencies: [0]
|
|
968
|
+
},
|
|
969
|
+
actions: {
|
|
970
|
+
flexDirection: "row",
|
|
971
|
+
flexWrap: "wrap",
|
|
972
|
+
gap: theme.spacing(1),
|
|
973
|
+
uni__dependencies: [0]
|
|
974
|
+
},
|
|
975
|
+
title: {
|
|
976
|
+
color: theme.colors.hi,
|
|
977
|
+
fontSize: theme.fontSize.base,
|
|
978
|
+
fontWeight: "600",
|
|
979
|
+
uni__dependencies: [0]
|
|
980
|
+
},
|
|
981
|
+
meta: {
|
|
982
|
+
color: theme.colors.dim,
|
|
983
|
+
fontSize: theme.fontSize.sm,
|
|
984
|
+
uni__dependencies: [0]
|
|
985
|
+
},
|
|
986
|
+
error: {
|
|
987
|
+
color: theme.colors.red,
|
|
988
|
+
fontSize: theme.fontSize.sm,
|
|
989
|
+
uni__dependencies: [0]
|
|
990
|
+
},
|
|
991
|
+
diff: (marker) => ({
|
|
992
|
+
fontFamily: fontOf(theme.fontFamily.mono),
|
|
993
|
+
fontSize: theme.fontSize.sm,
|
|
994
|
+
color: marker === "+" ? theme.colors.green : marker === "-" ? theme.colors.red : theme.colors.text,
|
|
995
|
+
paddingVertical: theme.spacing(1),
|
|
996
|
+
uni__dependencies: [0]
|
|
997
|
+
})
|
|
998
|
+
}));
|
|
999
|
+
//#endregion
|
|
1000
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/_components/WorktreesActivitySection.native.tsx
|
|
1001
|
+
function WorktreesActivitySection({ sessionId, openTransientTab }) {
|
|
1002
|
+
const git = useGitChanges(sessionId);
|
|
1003
|
+
const state = useWorktrees(sessionId);
|
|
1004
|
+
const worktrees = state.worktrees.filter((item) => item.sessionId !== sessionId);
|
|
1005
|
+
return /* @__PURE__ */ jsxs(View, { children: [
|
|
1006
|
+
git.changes ? /* @__PURE__ */ jsxs(Button, {
|
|
1007
|
+
variant: "ghost",
|
|
1008
|
+
testID: "activity-diff-changes",
|
|
1009
|
+
onPress: () => openTransientTab(reviewTab()),
|
|
1010
|
+
children: [
|
|
1011
|
+
git.changes.branch ?? "Detached HEAD",
|
|
1012
|
+
" · +",
|
|
1013
|
+
git.changes.added,
|
|
1014
|
+
" -",
|
|
1015
|
+
git.changes.removed
|
|
1016
|
+
]
|
|
1017
|
+
}) : null,
|
|
1018
|
+
state.pending ? /* @__PURE__ */ jsx(ListRow, { title: state.pending }) : null,
|
|
1019
|
+
worktrees.map((item) => /* @__PURE__ */ jsx(ListRow, {
|
|
1020
|
+
leading: /* @__PURE__ */ jsx(Dot, { tone: item.orphaned ? "red" : item.sessionId === null ? "muted" : "blue" }),
|
|
1021
|
+
title: /* @__PURE__ */ jsx(Button, {
|
|
1022
|
+
variant: "ghost",
|
|
1023
|
+
onPress: () => openTransientTab(worktreesTab()),
|
|
1024
|
+
children: item.branch
|
|
1025
|
+
}),
|
|
1026
|
+
description: [item.orphaned ? "Orphaned" : "", item.unowned ? "Unowned" : ""].filter(Boolean).join(" · ")
|
|
1027
|
+
}, item.id)),
|
|
1028
|
+
worktrees.length === 0 && state.pending === void 0 ? /* @__PURE__ */ jsx(Button, {
|
|
1029
|
+
variant: "ghost",
|
|
1030
|
+
testID: "activity-git-create",
|
|
1031
|
+
disabled: sessionId === null,
|
|
1032
|
+
onPress: () => openTransientTab(worktreesTab()),
|
|
1033
|
+
children: "Create a worktree"
|
|
1034
|
+
}) : null
|
|
1035
|
+
] });
|
|
1036
|
+
}
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/Worktrees.activity.git.native.tsx
|
|
1039
|
+
var Worktrees_activity_git_native_default = defineFill({ component: WorktreesActivitySection });
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region src/extensions/workspaces/sessions/(frontend)/lifecycle/start.native.ts
|
|
1042
|
+
var start_native_default = defineWebLifecycle((runtime) => {
|
|
1043
|
+
const stopWorktrees = startWorktreeRuntime(runtime);
|
|
1044
|
+
const stopChanges = startGitChangesRuntime(runtime);
|
|
1045
|
+
return () => {
|
|
1046
|
+
stopChanges();
|
|
1047
|
+
stopWorktrees();
|
|
1048
|
+
};
|
|
1049
|
+
});
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region generated/native.ts
|
|
1052
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
1053
|
+
...identity,
|
|
1054
|
+
...value
|
|
1055
|
+
};
|
|
1056
|
+
const nativePlugin = defineNativePlugin({
|
|
1057
|
+
id: "git",
|
|
1058
|
+
session: {
|
|
1059
|
+
get activityGroups() {
|
|
1060
|
+
return [via({ name: "git" }, git_native_default)];
|
|
1061
|
+
},
|
|
1062
|
+
get channels() {
|
|
1063
|
+
return [via({ channel: "git_changes" }, git_changes_native_default), via({ channel: "git_worktrees" }, git_worktrees_native_default)];
|
|
1064
|
+
},
|
|
1065
|
+
get fills() {
|
|
1066
|
+
return [via({
|
|
1067
|
+
slot: "activity.git",
|
|
1068
|
+
id: "worktrees"
|
|
1069
|
+
}, Worktrees_activity_git_native_default)];
|
|
1070
|
+
},
|
|
1071
|
+
get start() {
|
|
1072
|
+
return start_native_default;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
//#endregion
|
|
1077
|
+
export { nativePlugin };
|
|
1078
|
+
|
|
1079
|
+
//# sourceMappingURL=native.mjs.map
|