@agimon-ai/po-file-edit 0.0.0-alpha.0 → 0.0.0-alpha.2
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/extensions/native.mjs +1004 -0
- package/dist/extensions/native.mjs.map +1 -0
- package/dist/extensions/server.cjs +10 -10
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.mjs +10 -10
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +37 -45
- package/dist/extensions/web.mjs.map +1 -1
- package/package.json +38 -10
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineNativePlugin, defineSessionStore, 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 { Button, CodeBlock, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, KebabIcon, Label, ListRow, Markdown, SegmentedControl, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Textarea, fontOf } from "@agimon-ai/po-native-components";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
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/webFiles.ts
|
|
10
|
+
/** The frame type the hub publishes and the web plugin claims; declared in the poWeb block too. */
|
|
11
|
+
const filesChannelType = "file_edits";
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/extensions/workspaces/sessions/(frontend)/activity-group/files.native.ts
|
|
14
|
+
var files_native_default = defineActivityGroup({
|
|
15
|
+
keys: "e f",
|
|
16
|
+
statusKey: "doom-file-edit-files",
|
|
17
|
+
hideWhenEmpty: true,
|
|
18
|
+
marksBackgroundWork: false,
|
|
19
|
+
order: 15
|
|
20
|
+
});
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/filesModel.ts
|
|
23
|
+
function isRecord$1(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function isItem(value) {
|
|
27
|
+
return isRecord$1(value) && typeof value.path === "string" && typeof value.relPath === "string" && typeof value.tool === "string" && typeof value.at === "number" && typeof value.count === "number";
|
|
28
|
+
}
|
|
29
|
+
/** The shared wire gate for browser and native stores. */
|
|
30
|
+
function parseFilesPayload(input) {
|
|
31
|
+
if (!isRecord$1(input) || !Array.isArray(input.items)) return null;
|
|
32
|
+
return { items: input.items.filter(isItem) };
|
|
33
|
+
}
|
|
34
|
+
const files = defineSessionStore({
|
|
35
|
+
items: [],
|
|
36
|
+
detail: {},
|
|
37
|
+
loading: [],
|
|
38
|
+
errors: {},
|
|
39
|
+
comments: []
|
|
40
|
+
});
|
|
41
|
+
/** The plugin's session data channel: 'file_edits' payloads into the store. */
|
|
42
|
+
const filesChannel = files.channel({
|
|
43
|
+
channel: filesChannelType,
|
|
44
|
+
parse: parseFilesPayload,
|
|
45
|
+
reduce: (current, { items }) => ({
|
|
46
|
+
...current,
|
|
47
|
+
items
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
function markLoading(sessionId, filePath) {
|
|
51
|
+
files.update(sessionId, (current) => current.loading.includes(filePath) ? current : {
|
|
52
|
+
...current,
|
|
53
|
+
loading: [...current.loading, filePath],
|
|
54
|
+
errors: withoutKey(current.errors, filePath)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function storeDetail(sessionId, detail) {
|
|
58
|
+
files.update(sessionId, (current) => ({
|
|
59
|
+
...current,
|
|
60
|
+
detail: {
|
|
61
|
+
...current.detail,
|
|
62
|
+
[detail.path]: detail
|
|
63
|
+
},
|
|
64
|
+
loading: current.loading.filter((path) => path !== detail.path),
|
|
65
|
+
errors: withoutKey(current.errors, detail.path)
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
function storeError(sessionId, filePath, message) {
|
|
69
|
+
files.update(sessionId, (current) => ({
|
|
70
|
+
...current,
|
|
71
|
+
loading: current.loading.filter((path) => path !== filePath),
|
|
72
|
+
errors: {
|
|
73
|
+
...current.errors,
|
|
74
|
+
[filePath]: message
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
function addComment(sessionId, comment) {
|
|
79
|
+
files.update(sessionId, (current) => ({
|
|
80
|
+
...current,
|
|
81
|
+
comments: [...current.comments, comment]
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
function removeComment(sessionId, id) {
|
|
85
|
+
files.update(sessionId, (current) => ({
|
|
86
|
+
...current,
|
|
87
|
+
comments: current.comments.filter((comment) => comment.id !== id)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
function clearComments(sessionId, filePath) {
|
|
91
|
+
files.update(sessionId, (current) => ({
|
|
92
|
+
...current,
|
|
93
|
+
comments: current.comments.filter((comment) => comment.path !== filePath)
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
function withoutKey(source, key) {
|
|
97
|
+
return Object.fromEntries(Object.entries(source).filter(([entry]) => entry !== key));
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/file-edits.native.ts
|
|
101
|
+
var file_edits_native_default = defineChannelFile(filesChannel);
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/fileIdentity.ts
|
|
104
|
+
/** A short, stable fingerprint of a string; enough to separate two paths in an id. */
|
|
105
|
+
function fingerprint(value) {
|
|
106
|
+
let hash = 2166136261;
|
|
107
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
108
|
+
hash ^= value.charCodeAt(index);
|
|
109
|
+
hash = Math.imul(hash, 16777619);
|
|
110
|
+
}
|
|
111
|
+
return (hash >>> 0).toString(36);
|
|
112
|
+
}
|
|
113
|
+
/** The transient tab id for one file; stable, so reopening focuses rather than duplicates. */
|
|
114
|
+
function fileTabId(filePath) {
|
|
115
|
+
const slug = filePath.replaceAll(/[^a-zA-Z0-9]+/gu, "-").replace(/^-|-$/gu, "");
|
|
116
|
+
return `files-file-${fingerprint(filePath)}-${slug.slice(-40)}`;
|
|
117
|
+
}
|
|
118
|
+
const api = createApiClient({
|
|
119
|
+
detail: {
|
|
120
|
+
method: "GET",
|
|
121
|
+
path: "/detail",
|
|
122
|
+
query: ["path"],
|
|
123
|
+
response: void 0
|
|
124
|
+
},
|
|
125
|
+
preview: {
|
|
126
|
+
method: "GET",
|
|
127
|
+
path: "/preview",
|
|
128
|
+
query: ["path"],
|
|
129
|
+
response: void 0
|
|
130
|
+
},
|
|
131
|
+
save: {
|
|
132
|
+
method: "PUT",
|
|
133
|
+
path: "/content",
|
|
134
|
+
response: void 0
|
|
135
|
+
},
|
|
136
|
+
/** The same path as `save`; the method is the whole of the difference. */
|
|
137
|
+
remove: {
|
|
138
|
+
method: "DELETE",
|
|
139
|
+
path: "/content",
|
|
140
|
+
query: ["path"]
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* The host's own bytes route, not this package's.
|
|
144
|
+
*
|
|
145
|
+
* `host: true` hangs it off the session root rather than under `/plugins`,
|
|
146
|
+
* which is how the cockpit already serves it. Declaring it here is the first
|
|
147
|
+
* time anything records that this package's previews depend on it.
|
|
148
|
+
*/
|
|
149
|
+
file: {
|
|
150
|
+
method: "GET",
|
|
151
|
+
path: "/file",
|
|
152
|
+
raw: true,
|
|
153
|
+
query: ["path"],
|
|
154
|
+
host: true
|
|
155
|
+
}
|
|
156
|
+
}, {
|
|
157
|
+
scopes: ["session"],
|
|
158
|
+
basePath: "file-edits",
|
|
159
|
+
transport: nativeApiTransport,
|
|
160
|
+
sessionAddress: sessionApiAddress
|
|
161
|
+
});
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/types/fileEditsApi.ts
|
|
164
|
+
/** Query parameter naming the file a request is about. */
|
|
165
|
+
const PATH_QUERY_PARAM = "path";
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/filesApi.native.ts
|
|
168
|
+
/**
|
|
169
|
+
* The page's half of this package's session API: one file's history, the
|
|
170
|
+
* manual save, and the deletion.
|
|
171
|
+
*
|
|
172
|
+
* These are adapters now, not a transport. The generated client owns the URL
|
|
173
|
+
* and the sealed transport with it, so nothing here spells a route or reaches
|
|
174
|
+
* for `fetch`. What stays is this package's own vocabulary: which refusals the
|
|
175
|
+
* reader is shown, and the stale-save contract the editor depends on.
|
|
176
|
+
*
|
|
177
|
+
* Sealing still matters and still happens, one layer down. What travels here is
|
|
178
|
+
* file contents in both directions, which is the worst thing in the cockpit to
|
|
179
|
+
* hand a tunnel's relay in the clear.
|
|
180
|
+
*/
|
|
181
|
+
const UNREACHABLE = "The session is unreachable.";
|
|
182
|
+
function isRecord(value) {
|
|
183
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
184
|
+
}
|
|
185
|
+
/** The message a reader is shown: the route's own words, or why there were none. */
|
|
186
|
+
function messageOf(result) {
|
|
187
|
+
if (result.status === 0) return UNREACHABLE;
|
|
188
|
+
return result.error === "" ? `The session answered ${result.status}.` : result.error;
|
|
189
|
+
}
|
|
190
|
+
async function fetchFileDetail(sessionId, filePath) {
|
|
191
|
+
const result = await api.session(sessionId).detail({ query: { [PATH_QUERY_PARAM]: filePath } });
|
|
192
|
+
if (!result.ok) return {
|
|
193
|
+
ok: false,
|
|
194
|
+
error: messageOf(result)
|
|
195
|
+
};
|
|
196
|
+
if (!isRecord(result.data)) return {
|
|
197
|
+
ok: false,
|
|
198
|
+
error: "The session answered with no detail."
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
detail: result.data
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function saveFileContent(sessionId, filePath, expectedHash, content) {
|
|
206
|
+
const result = await api.session(sessionId).save({ body: {
|
|
207
|
+
path: filePath,
|
|
208
|
+
expectedHash,
|
|
209
|
+
content
|
|
210
|
+
} });
|
|
211
|
+
if (result.status === 409) {
|
|
212
|
+
const data = result.ok ? void 0 : result.data;
|
|
213
|
+
const hash = isRecord(data) && typeof data.hash === "string" ? data.hash : void 0;
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
stale: true,
|
|
217
|
+
error: result.ok ? "The file changed since it was opened." : messageOf(result),
|
|
218
|
+
...hash === void 0 ? {} : { hash }
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (!result.ok) return {
|
|
222
|
+
ok: false,
|
|
223
|
+
stale: false,
|
|
224
|
+
error: messageOf(result)
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
hash: isRecord(result.data) && typeof result.data.hash === "string" ? result.data.hash : ""
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Removes the file from disk.
|
|
233
|
+
*
|
|
234
|
+
* The route takes only a path, with no hash to prove the reader saw what is
|
|
235
|
+
* there now: a deletion discards the file whatever it holds, so a staleness
|
|
236
|
+
* check would guard nothing. The confirmation in front of it is the guard.
|
|
237
|
+
*/
|
|
238
|
+
async function deleteFile(sessionId, filePath) {
|
|
239
|
+
const result = await api.session(sessionId).remove({ query: { [PATH_QUERY_PARAM]: filePath } });
|
|
240
|
+
return result.ok ? { ok: true } : {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: messageOf(result)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/** Binds a save to this package's guarded sealed route and one file version. */
|
|
246
|
+
async function prepareFileSave(sessionId, path, signal) {
|
|
247
|
+
const load = async (headers, signal) => {
|
|
248
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
249
|
+
const result = await api.session(sessionId).file({
|
|
250
|
+
query: { path },
|
|
251
|
+
headers,
|
|
252
|
+
signal
|
|
253
|
+
});
|
|
254
|
+
if (!result.ok && result.status !== 416) throw new Error(result.error || "File could not be read.");
|
|
255
|
+
if (!result.response) throw new Error("File could not be read.");
|
|
256
|
+
return result.response;
|
|
257
|
+
};
|
|
258
|
+
const probe = await load({ Range: "bytes=0-0" }, signal);
|
|
259
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
260
|
+
const range = /^bytes 0-0\/(\d+)$/u.exec(probe.headers.get("content-range") ?? "");
|
|
261
|
+
const empty = probe.status === 416 && probe.headers.get("content-range") === "bytes */0" || probe.status === 200 && probe.headers.get("content-length") === "0";
|
|
262
|
+
if (!empty && (probe.status !== 206 || !range)) throw new Error("The file does not support bounded downloads.");
|
|
263
|
+
const size = empty ? 0 : Number(range[1]);
|
|
264
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new Error("Invalid file size.");
|
|
265
|
+
if ((await probe.arrayBuffer()).byteLength !== (empty ? 0 : 1)) throw new Error("Invalid file response.");
|
|
266
|
+
const version = probe.headers.get("etag");
|
|
267
|
+
if (!version) throw new Error("The file has no version validator.");
|
|
268
|
+
return {
|
|
269
|
+
name: path.split(/[\\/]/u).at(-1) ?? "",
|
|
270
|
+
mimeType: probe.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream",
|
|
271
|
+
size,
|
|
272
|
+
signal,
|
|
273
|
+
read: async (offset, count, signal) => {
|
|
274
|
+
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(count) || offset < 0 || count < 1 || count > 1048576 || offset + count > size) throw new Error("Invalid download range.");
|
|
275
|
+
const end = offset + count - 1;
|
|
276
|
+
const response = await load({
|
|
277
|
+
Range: `bytes=${offset}-${end}`,
|
|
278
|
+
"If-Match": version
|
|
279
|
+
}, signal);
|
|
280
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
281
|
+
if (response.status !== 206 || response.headers.get("etag") !== version || response.headers.get("content-range") !== `bytes ${offset}-${end}/${size}`) throw new Error("The file changed or its range response is invalid.");
|
|
282
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
283
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
284
|
+
if (bytes.byteLength !== count) throw new Error("The file response is incomplete.");
|
|
285
|
+
return bytes;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/FilePreview.native.tsx
|
|
291
|
+
const MEDIA = /\.(?:png|jpe?g|gif|webp|bmp|svg|ico|mp3|wav|m4a|ogg|mp4|mov|webm|pdf)$/iu;
|
|
292
|
+
const MAX_MEDIA_BYTES = 5242880;
|
|
293
|
+
function base64(bytes) {
|
|
294
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
295
|
+
const parts = [];
|
|
296
|
+
let chunk = "";
|
|
297
|
+
for (let offset = 0; offset < bytes.length; offset += 3) {
|
|
298
|
+
const first = bytes[offset];
|
|
299
|
+
const second = bytes[offset + 1];
|
|
300
|
+
const third = bytes[offset + 2];
|
|
301
|
+
chunk += alphabet[first >> 2] + alphabet[(first & 3) << 4 | (second ?? 0) >> 4] + (second === void 0 ? "=" : alphabet[(second & 15) << 2 | (third ?? 0) >> 6]) + (third === void 0 ? "=" : alphabet[third & 63]);
|
|
302
|
+
if (chunk.length >= 8192) {
|
|
303
|
+
parts.push(chunk);
|
|
304
|
+
chunk = "";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
parts.push(chunk);
|
|
308
|
+
return parts.join("");
|
|
309
|
+
}
|
|
310
|
+
function SessionMedia({ sessionId, path, renderMediaPreview }) {
|
|
311
|
+
const [loaded, setLoaded] = useState();
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
if (sessionId === null) return;
|
|
314
|
+
const controller = new AbortController();
|
|
315
|
+
(async () => {
|
|
316
|
+
const result = await api.session(sessionId).file({
|
|
317
|
+
query: { path },
|
|
318
|
+
signal: controller.signal
|
|
319
|
+
});
|
|
320
|
+
if (!result.ok) throw new Error(result.error || `Media could not be loaded (${result.status}).`);
|
|
321
|
+
if (Number(result.response.headers.get("content-length")) > MAX_MEDIA_BYTES) throw new Error("This media is too large to preview.");
|
|
322
|
+
const bytes = new Uint8Array(await result.response.arrayBuffer());
|
|
323
|
+
if (bytes.length > MAX_MEDIA_BYTES) throw new Error("This media is too large to preview.");
|
|
324
|
+
const type = result.response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
325
|
+
if (!/^(?:image\/[a-z0-9.+-]+|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+|application\/pdf)$/u.test(type)) throw new Error("This media type cannot be previewed.");
|
|
326
|
+
if (!controller.signal.aborted) setLoaded({
|
|
327
|
+
sessionId,
|
|
328
|
+
path,
|
|
329
|
+
type,
|
|
330
|
+
data: base64(bytes)
|
|
331
|
+
});
|
|
332
|
+
})().catch((failure) => {
|
|
333
|
+
if (!controller.signal.aborted) setLoaded({
|
|
334
|
+
sessionId,
|
|
335
|
+
path,
|
|
336
|
+
error: failure instanceof Error ? failure.message : String(failure)
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
return () => controller.abort();
|
|
340
|
+
}, [sessionId, path]);
|
|
341
|
+
const current = loaded?.sessionId === sessionId && loaded.path === path ? loaded : void 0;
|
|
342
|
+
if (!current?.data) return /* @__PURE__ */ jsx(Text, {
|
|
343
|
+
accessibilityRole: current?.error ? "alert" : "text",
|
|
344
|
+
style: current?.error ? styles$1.error : styles$1.meta,
|
|
345
|
+
children: current?.error ?? "Loading media..."
|
|
346
|
+
});
|
|
347
|
+
return renderMediaPreview ? /* @__PURE__ */ jsx(Fragment, { children: renderMediaPreview({
|
|
348
|
+
key: `file-media-${sessionId}-${path}`,
|
|
349
|
+
data: current.data,
|
|
350
|
+
mimeType: current.type,
|
|
351
|
+
title: path,
|
|
352
|
+
path
|
|
353
|
+
}) }) : /* @__PURE__ */ jsx(Text, {
|
|
354
|
+
style: styles$1.error,
|
|
355
|
+
children: "This host cannot display this media preview."
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/** Rendering is separate from reading: only the guarded package API supplies file content or bytes. */
|
|
359
|
+
function FilePreview({ path, working, ...props }) {
|
|
360
|
+
if (MEDIA.test(path)) return /* @__PURE__ */ jsx(SessionMedia, {
|
|
361
|
+
sessionId: props.sessionId,
|
|
362
|
+
path,
|
|
363
|
+
renderMediaPreview: props.renderMediaPreview
|
|
364
|
+
});
|
|
365
|
+
if (working.unavailable) return /* @__PURE__ */ jsx(Text, {
|
|
366
|
+
style: styles$1.meta,
|
|
367
|
+
children: working.reason ?? "this file cannot be shown"
|
|
368
|
+
});
|
|
369
|
+
if (/\.html?$/iu.test(path)) return props.renderFilePreview ? /* @__PURE__ */ jsx(View, {
|
|
370
|
+
style: styles$1.html,
|
|
371
|
+
children: props.renderFilePreview({
|
|
372
|
+
key: `file-html-${props.sessionId}-${path}`,
|
|
373
|
+
html: working.content,
|
|
374
|
+
title: path
|
|
375
|
+
})
|
|
376
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
377
|
+
style: styles$1.error,
|
|
378
|
+
children: "This host cannot display HTML previews."
|
|
379
|
+
});
|
|
380
|
+
if (/\.(?:md|markdown)$/iu.test(path)) return /* @__PURE__ */ jsx(Markdown, {
|
|
381
|
+
text: working.content,
|
|
382
|
+
onFileLink: (file) => {
|
|
383
|
+
const tab = props.fileTabFor(file);
|
|
384
|
+
return tab ? () => props.openTransientTab(tab) : void 0;
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
388
|
+
text: working.content,
|
|
389
|
+
language: path.split(".").at(-1)
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const styles$1 = StyleSheet.create((theme) => ({
|
|
393
|
+
html: {
|
|
394
|
+
minHeight: theme.spacing(90),
|
|
395
|
+
uni__dependencies: [0]
|
|
396
|
+
},
|
|
397
|
+
meta: {
|
|
398
|
+
fontSize: theme.fontSize.sm,
|
|
399
|
+
color: theme.colors.dim,
|
|
400
|
+
uni__dependencies: [0]
|
|
401
|
+
},
|
|
402
|
+
error: {
|
|
403
|
+
fontSize: theme.fontSize.sm,
|
|
404
|
+
color: theme.colors.red,
|
|
405
|
+
uni__dependencies: [0]
|
|
406
|
+
}
|
|
407
|
+
}));
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/FilePanel.native.tsx
|
|
410
|
+
function Diff({ hunks, comment }) {
|
|
411
|
+
return /* @__PURE__ */ jsx(View, {
|
|
412
|
+
style: styles.diff,
|
|
413
|
+
children: hunks.map((hunk, index) => /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Text, {
|
|
414
|
+
style: styles.meta,
|
|
415
|
+
children: ["line ", hunk.start]
|
|
416
|
+
}), hunk.rows.map((row, position) => /* @__PURE__ */ jsx(Button, {
|
|
417
|
+
variant: "ghost",
|
|
418
|
+
accessibilityLabel: `comment on ${row.marker === "-" ? "removed" : "new"} line ${row.line}`,
|
|
419
|
+
onPress: () => comment({
|
|
420
|
+
snippet: row.content,
|
|
421
|
+
side: row.marker === "-" ? "old" : "new",
|
|
422
|
+
startLine: row.line,
|
|
423
|
+
endLine: row.line
|
|
424
|
+
}),
|
|
425
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
426
|
+
selectable: true,
|
|
427
|
+
style: row.marker === "+" ? styles.added : row.marker === "-" ? styles.removed : styles.code,
|
|
428
|
+
children: [
|
|
429
|
+
row.marker,
|
|
430
|
+
" ",
|
|
431
|
+
row.line,
|
|
432
|
+
" ",
|
|
433
|
+
row.content
|
|
434
|
+
]
|
|
435
|
+
})
|
|
436
|
+
}, `${row.line}-${position}`))] }, `${hunk.start}-${index}`))
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function ReviewSheet({ selection, close, submit }) {
|
|
440
|
+
const [body, setBody] = useState("");
|
|
441
|
+
const [snippet, setSnippet] = useState(selection.snippet);
|
|
442
|
+
return /* @__PURE__ */ jsx(Sheet, {
|
|
443
|
+
open: true,
|
|
444
|
+
onOpenChange: (open) => {
|
|
445
|
+
if (!open) close();
|
|
446
|
+
},
|
|
447
|
+
children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "review comment" }) }), /* @__PURE__ */ jsxs(SheetBody, { children: [
|
|
448
|
+
/* @__PURE__ */ jsx(Label, { children: "quotation" }),
|
|
449
|
+
/* @__PURE__ */ jsx(Textarea, {
|
|
450
|
+
accessibilityLabel: "quotation",
|
|
451
|
+
value: snippet,
|
|
452
|
+
onChangeText: setSnippet,
|
|
453
|
+
rows: 3
|
|
454
|
+
}),
|
|
455
|
+
/* @__PURE__ */ jsx(Label, { children: "comment" }),
|
|
456
|
+
/* @__PURE__ */ jsx(Textarea, {
|
|
457
|
+
accessibilityLabel: "comment",
|
|
458
|
+
value: body,
|
|
459
|
+
onChangeText: setBody,
|
|
460
|
+
rows: 3
|
|
461
|
+
}),
|
|
462
|
+
/* @__PURE__ */ jsxs(View, {
|
|
463
|
+
style: styles.actions,
|
|
464
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
465
|
+
variant: "ghost",
|
|
466
|
+
onPress: close,
|
|
467
|
+
children: "cancel"
|
|
468
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
469
|
+
variant: "primary",
|
|
470
|
+
disabled: !body.trim(),
|
|
471
|
+
onPress: () => submit(body.trim(), snippet),
|
|
472
|
+
children: "add comment"
|
|
473
|
+
})]
|
|
474
|
+
})
|
|
475
|
+
] })] })
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/** File writes retain the existing containment and stale-hash guards; deletion always asks first. */
|
|
479
|
+
function FilePanel({ filePath, relPath, ...props }) {
|
|
480
|
+
const { sessionId, sendSessionFrame, closeTransientTab } = props;
|
|
481
|
+
const subscribe = useCallback((listener) => {
|
|
482
|
+
const subscription = files.store.subscribe(listener);
|
|
483
|
+
return () => subscription.unsubscribe();
|
|
484
|
+
}, []);
|
|
485
|
+
const session = useSyncExternalStore(subscribe, () => files.select(files.store.state, sessionId));
|
|
486
|
+
const detail = session.detail[filePath];
|
|
487
|
+
const working = detail?.working;
|
|
488
|
+
const comments = session.comments.filter((comment) => comment.path === filePath);
|
|
489
|
+
const [view, setView] = useState("preview");
|
|
490
|
+
const [source, setSource] = useState();
|
|
491
|
+
const [expanded, setExpanded] = useState();
|
|
492
|
+
const [selection, setSelection] = useState();
|
|
493
|
+
const [draft, setDraft] = useState();
|
|
494
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
495
|
+
const [busy, setBusy] = useState(false);
|
|
496
|
+
const [note, setNote] = useState();
|
|
497
|
+
const [reviewError, setReviewError] = useState();
|
|
498
|
+
const outside = relPath === ".." || relPath.startsWith("../") || relPath.startsWith("/") || relPath.startsWith("..\\") || /^[a-z]:[\\/]/iu.test(relPath);
|
|
499
|
+
const writable = sessionId !== null && !outside && working !== void 0 && !working.unavailable;
|
|
500
|
+
const revision = session.items.find((item) => item.path === filePath)?.count ?? 0;
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
if (sessionId === null) return;
|
|
503
|
+
let cancelled = false;
|
|
504
|
+
markLoading(sessionId, filePath);
|
|
505
|
+
fetchFileDetail(sessionId, filePath).then((result) => {
|
|
506
|
+
if (cancelled) return;
|
|
507
|
+
if (result.ok) {
|
|
508
|
+
storeDetail(sessionId, result.detail);
|
|
509
|
+
setSource(void 0);
|
|
510
|
+
setNote(void 0);
|
|
511
|
+
} else storeError(sessionId, filePath, result.error);
|
|
512
|
+
}).catch((failure) => {
|
|
513
|
+
if (!cancelled) storeError(sessionId, filePath, failure instanceof Error ? failure.message : String(failure));
|
|
514
|
+
});
|
|
515
|
+
return () => {
|
|
516
|
+
cancelled = true;
|
|
517
|
+
};
|
|
518
|
+
}, [
|
|
519
|
+
sessionId,
|
|
520
|
+
filePath,
|
|
521
|
+
revision
|
|
522
|
+
]);
|
|
523
|
+
const downloadController = useRef(void 0);
|
|
524
|
+
useEffect(() => () => downloadController.current?.abort(), [sessionId, relPath]);
|
|
525
|
+
const download = async () => {
|
|
526
|
+
if (sessionId === null || outside || busy || !props.nativeSaveFile) return;
|
|
527
|
+
const controller = new AbortController();
|
|
528
|
+
downloadController.current = controller;
|
|
529
|
+
setBusy(true);
|
|
530
|
+
try {
|
|
531
|
+
await props.nativeSaveFile(await prepareFileSave(sessionId, relPath, controller.signal));
|
|
532
|
+
if (!controller.signal.aborted) setNote("saved");
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (!controller.signal.aborted) setNote(error instanceof Error ? error.message : String(error));
|
|
535
|
+
} finally {
|
|
536
|
+
setBusy(false);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
const save = async () => {
|
|
540
|
+
if (!writable || sessionId === null || !working || source === void 0 || busy) return;
|
|
541
|
+
setBusy(true);
|
|
542
|
+
try {
|
|
543
|
+
const result = await saveFileContent(sessionId, filePath, working.hash, source);
|
|
544
|
+
if (!result.ok) {
|
|
545
|
+
setNote(result.error);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
setNote("saved");
|
|
549
|
+
const refreshed = await fetchFileDetail(sessionId, filePath);
|
|
550
|
+
if (refreshed.ok) {
|
|
551
|
+
storeDetail(sessionId, refreshed.detail);
|
|
552
|
+
setSource(void 0);
|
|
553
|
+
} else storeError(sessionId, filePath, refreshed.error);
|
|
554
|
+
} catch (failure) {
|
|
555
|
+
setNote(failure instanceof Error ? failure.message : String(failure));
|
|
556
|
+
} finally {
|
|
557
|
+
setBusy(false);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
const remove = async () => {
|
|
561
|
+
if (!writable || sessionId === null || busy) return;
|
|
562
|
+
setBusy(true);
|
|
563
|
+
try {
|
|
564
|
+
const result = await deleteFile(sessionId, filePath);
|
|
565
|
+
if (!result.ok) {
|
|
566
|
+
storeError(sessionId, filePath, result.error);
|
|
567
|
+
setConfirmingDelete(false);
|
|
568
|
+
} else {
|
|
569
|
+
setConfirmingDelete(false);
|
|
570
|
+
closeTransientTab(fileTabId(filePath));
|
|
571
|
+
}
|
|
572
|
+
} catch (failure) {
|
|
573
|
+
storeError(sessionId, filePath, failure instanceof Error ? failure.message : String(failure));
|
|
574
|
+
setConfirmingDelete(false);
|
|
575
|
+
} finally {
|
|
576
|
+
setBusy(false);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
const sendReview = () => {
|
|
580
|
+
if (sessionId === null || comments.length === 0) return;
|
|
581
|
+
const message = `I left ${comments.length} review ${comments.length === 1 ? "comment" : "comments"} on files you changed. Please address them.\n\n` + comments.map((comment) => {
|
|
582
|
+
return `### ${comment.startLine === void 0 ? comment.relPath : comment.side === "old" ? `${comment.relPath} (removed line ${comment.startLine})` : `${comment.relPath}:${comment.startLine}`}\n\n${comment.snippet.split("\n").slice(0, 20).join("\n").slice(0, 2e3).split("\n").map((line) => `> ${line}`).join("\n")}\n\n${comment.body.trim()}`;
|
|
583
|
+
}).join("\n\n");
|
|
584
|
+
try {
|
|
585
|
+
sendSessionFrame(sessionId, {
|
|
586
|
+
type: "follow_up",
|
|
587
|
+
message
|
|
588
|
+
});
|
|
589
|
+
clearComments(sessionId, filePath);
|
|
590
|
+
setReviewError(void 0);
|
|
591
|
+
} catch (failure) {
|
|
592
|
+
setReviewError(failure instanceof Error ? failure.message : String(failure));
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
return /* @__PURE__ */ jsxs(ScrollView, {
|
|
596
|
+
testID: "files-file-panel",
|
|
597
|
+
contentContainerStyle: styles.panel,
|
|
598
|
+
children: [
|
|
599
|
+
/* @__PURE__ */ jsxs(View, {
|
|
600
|
+
style: styles.header,
|
|
601
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
602
|
+
selectable: true,
|
|
603
|
+
style: styles.path,
|
|
604
|
+
children: relPath
|
|
605
|
+
}), /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
606
|
+
variant: "ghost",
|
|
607
|
+
size: "icon",
|
|
608
|
+
accessibilityLabel: "file actions",
|
|
609
|
+
children: /* @__PURE__ */ jsx(KebabIcon, {})
|
|
610
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, { children: [
|
|
611
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
612
|
+
disabled: sessionId === null || outside || busy || !props.nativeSaveFile,
|
|
613
|
+
onSelect: () => void download(),
|
|
614
|
+
children: "download"
|
|
615
|
+
}),
|
|
616
|
+
busy ? /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
617
|
+
onSelect: () => downloadController.current?.abort(),
|
|
618
|
+
children: "cancel download"
|
|
619
|
+
}) : null,
|
|
620
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
621
|
+
disabled: !writable,
|
|
622
|
+
onSelect: () => setView("edit"),
|
|
623
|
+
children: "edit"
|
|
624
|
+
}),
|
|
625
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
626
|
+
variant: "destructive",
|
|
627
|
+
disabled: !writable,
|
|
628
|
+
onSelect: () => setConfirmingDelete(true),
|
|
629
|
+
children: "delete"
|
|
630
|
+
}),
|
|
631
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
632
|
+
onSelect: () => closeTransientTab(fileTabId(filePath)),
|
|
633
|
+
children: "close"
|
|
634
|
+
})
|
|
635
|
+
] })] })]
|
|
636
|
+
}),
|
|
637
|
+
view !== "edit" && note ? /* @__PURE__ */ jsx(Text, {
|
|
638
|
+
style: note === "saved" ? styles.success : styles.error,
|
|
639
|
+
children: note
|
|
640
|
+
}) : null,
|
|
641
|
+
/* @__PURE__ */ jsx(SegmentedControl, {
|
|
642
|
+
accessibilityLabel: "file view",
|
|
643
|
+
options: [{
|
|
644
|
+
value: "preview",
|
|
645
|
+
label: "preview"
|
|
646
|
+
}, {
|
|
647
|
+
value: "diff",
|
|
648
|
+
label: "diff"
|
|
649
|
+
}],
|
|
650
|
+
value: view,
|
|
651
|
+
onValueChange: setView
|
|
652
|
+
}),
|
|
653
|
+
view === "edit" ? /* @__PURE__ */ jsx(Text, {
|
|
654
|
+
style: styles.meta,
|
|
655
|
+
children: "editing"
|
|
656
|
+
}) : null,
|
|
657
|
+
session.loading.includes(filePath) && !detail ? /* @__PURE__ */ jsx(Text, {
|
|
658
|
+
style: styles.meta,
|
|
659
|
+
children: "reading this file's history…"
|
|
660
|
+
}) : null,
|
|
661
|
+
session.errors[filePath] ? /* @__PURE__ */ jsx(Text, {
|
|
662
|
+
accessibilityRole: "alert",
|
|
663
|
+
style: styles.error,
|
|
664
|
+
children: session.errors[filePath]
|
|
665
|
+
}) : null,
|
|
666
|
+
detail ? view === "diff" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
667
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
668
|
+
style: styles.meta,
|
|
669
|
+
children: [
|
|
670
|
+
"+",
|
|
671
|
+
detail.cumulative.additions,
|
|
672
|
+
" -",
|
|
673
|
+
detail.cumulative.removals
|
|
674
|
+
]
|
|
675
|
+
}),
|
|
676
|
+
detail.cumulative.note ? /* @__PURE__ */ jsx(Text, {
|
|
677
|
+
style: styles.meta,
|
|
678
|
+
children: detail.cumulative.note
|
|
679
|
+
}) : null,
|
|
680
|
+
detail.cumulative.hunks ? /* @__PURE__ */ jsx(Diff, {
|
|
681
|
+
hunks: detail.cumulative.hunks,
|
|
682
|
+
comment: setDraft
|
|
683
|
+
}) : null,
|
|
684
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
685
|
+
style: styles.title,
|
|
686
|
+
children: [detail.versions.length, " changes this session"]
|
|
687
|
+
}),
|
|
688
|
+
detail.versions.map((version) => /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
689
|
+
variant: "ghost",
|
|
690
|
+
accessibilityState: { expanded: expanded === version.index },
|
|
691
|
+
onPress: () => setExpanded(expanded === version.index ? void 0 : version.index),
|
|
692
|
+
children: [
|
|
693
|
+
"#",
|
|
694
|
+
version.index,
|
|
695
|
+
" ",
|
|
696
|
+
version.tool,
|
|
697
|
+
" +",
|
|
698
|
+
version.additions,
|
|
699
|
+
" -",
|
|
700
|
+
version.removals,
|
|
701
|
+
version.origin === "scan" ? " · no baseline" : ""
|
|
702
|
+
]
|
|
703
|
+
}), expanded === version.index ? version.hunks ? /* @__PURE__ */ jsx(Diff, {
|
|
704
|
+
hunks: version.hunks,
|
|
705
|
+
comment: setDraft
|
|
706
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
707
|
+
style: styles.meta,
|
|
708
|
+
children: version.note ?? "nothing to show"
|
|
709
|
+
}) : null] }, version.index))
|
|
710
|
+
] }) : view === "edit" ? /* @__PURE__ */ jsxs(Fragment, { children: [writable ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
711
|
+
/* @__PURE__ */ jsx(Textarea, {
|
|
712
|
+
accessibilityLabel: "file source",
|
|
713
|
+
value: source ?? working?.content ?? "",
|
|
714
|
+
onChangeText: setSource,
|
|
715
|
+
rows: 12,
|
|
716
|
+
onSelectionChange: (event) => {
|
|
717
|
+
const content = source ?? working?.content ?? "";
|
|
718
|
+
const { start, end } = event.nativeEvent.selection;
|
|
719
|
+
setSelection(end > start ? {
|
|
720
|
+
snippet: content.slice(start, end),
|
|
721
|
+
startLine: content.slice(0, start).split("\n").length,
|
|
722
|
+
endLine: content.slice(0, end).split("\n").length
|
|
723
|
+
} : void 0);
|
|
724
|
+
}
|
|
725
|
+
}),
|
|
726
|
+
/* @__PURE__ */ jsxs(View, {
|
|
727
|
+
style: styles.actions,
|
|
728
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
729
|
+
variant: "primary",
|
|
730
|
+
disabled: source === void 0 || busy,
|
|
731
|
+
onPress: () => void save(),
|
|
732
|
+
children: "save to disk"
|
|
733
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
734
|
+
variant: "ghost",
|
|
735
|
+
disabled: source === void 0,
|
|
736
|
+
onPress: () => setSource(void 0),
|
|
737
|
+
children: "discard edits"
|
|
738
|
+
})]
|
|
739
|
+
}),
|
|
740
|
+
/* @__PURE__ */ jsx(Button, {
|
|
741
|
+
variant: "ghost",
|
|
742
|
+
disabled: !selection,
|
|
743
|
+
onPress: () => setDraft(selection),
|
|
744
|
+
children: "comment on selection"
|
|
745
|
+
})
|
|
746
|
+
] }) : /* @__PURE__ */ jsx(Text, {
|
|
747
|
+
style: styles.meta,
|
|
748
|
+
children: working?.reason ?? "this file cannot be edited"
|
|
749
|
+
}), note ? /* @__PURE__ */ jsx(Text, {
|
|
750
|
+
style: note === "saved" ? styles.success : styles.error,
|
|
751
|
+
children: note
|
|
752
|
+
}) : null] }) : working ? /* @__PURE__ */ jsx(FilePreview, {
|
|
753
|
+
...props,
|
|
754
|
+
path: relPath,
|
|
755
|
+
working: {
|
|
756
|
+
...working,
|
|
757
|
+
content: source ?? working.content
|
|
758
|
+
}
|
|
759
|
+
}) : null : null,
|
|
760
|
+
detail && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
|
|
761
|
+
variant: "ghost",
|
|
762
|
+
onPress: () => setDraft({ snippet: "" }),
|
|
763
|
+
children: "add review comment"
|
|
764
|
+
}) : null,
|
|
765
|
+
comments.length ? /* @__PURE__ */ jsxs(View, {
|
|
766
|
+
style: styles.diff,
|
|
767
|
+
children: [
|
|
768
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
769
|
+
variant: "outline",
|
|
770
|
+
disabled: sessionId === null,
|
|
771
|
+
onPress: sendReview,
|
|
772
|
+
children: [
|
|
773
|
+
"send review (",
|
|
774
|
+
comments.length,
|
|
775
|
+
")"
|
|
776
|
+
]
|
|
777
|
+
}),
|
|
778
|
+
reviewError ? /* @__PURE__ */ jsxs(Text, {
|
|
779
|
+
accessibilityRole: "alert",
|
|
780
|
+
style: styles.error,
|
|
781
|
+
children: ["review not sent: ", reviewError]
|
|
782
|
+
}) : null,
|
|
783
|
+
comments.map((comment) => /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Text, {
|
|
784
|
+
style: styles.meta,
|
|
785
|
+
children: [
|
|
786
|
+
comment.relPath,
|
|
787
|
+
comment.startLine === void 0 ? "" : `:${comment.startLine}`,
|
|
788
|
+
" · ",
|
|
789
|
+
comment.body
|
|
790
|
+
]
|
|
791
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
792
|
+
variant: "ghost",
|
|
793
|
+
onPress: () => {
|
|
794
|
+
if (sessionId !== null) removeComment(sessionId, comment.id);
|
|
795
|
+
},
|
|
796
|
+
children: "remove comment"
|
|
797
|
+
})] }, comment.id))
|
|
798
|
+
]
|
|
799
|
+
}) : null,
|
|
800
|
+
draft ? /* @__PURE__ */ jsx(ReviewSheet, {
|
|
801
|
+
selection: draft,
|
|
802
|
+
close: () => setDraft(void 0),
|
|
803
|
+
submit: (body, snippet) => {
|
|
804
|
+
if (sessionId !== null) addComment(sessionId, {
|
|
805
|
+
id: `${filePath}:${draft.side ?? "new"}:${draft.startLine ?? 0}:${comments.length}:${body.length}`,
|
|
806
|
+
path: filePath,
|
|
807
|
+
relPath,
|
|
808
|
+
...draft,
|
|
809
|
+
snippet,
|
|
810
|
+
body
|
|
811
|
+
});
|
|
812
|
+
setDraft(void 0);
|
|
813
|
+
}
|
|
814
|
+
}) : null,
|
|
815
|
+
confirmingDelete ? /* @__PURE__ */ jsx(Sheet, {
|
|
816
|
+
open: true,
|
|
817
|
+
onOpenChange: (open) => {
|
|
818
|
+
if (!open) setConfirmingDelete(false);
|
|
819
|
+
},
|
|
820
|
+
children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "delete file" }) }), /* @__PURE__ */ jsxs(SheetBody, { children: [/* @__PURE__ */ jsxs(Text, {
|
|
821
|
+
style: styles.meta,
|
|
822
|
+
children: [
|
|
823
|
+
"This removes ",
|
|
824
|
+
relPath,
|
|
825
|
+
" from disk. It cannot be undone here."
|
|
826
|
+
]
|
|
827
|
+
}), /* @__PURE__ */ jsxs(View, {
|
|
828
|
+
style: styles.actions,
|
|
829
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
830
|
+
variant: "ghost",
|
|
831
|
+
onPress: () => setConfirmingDelete(false),
|
|
832
|
+
children: "cancel"
|
|
833
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
834
|
+
variant: "danger",
|
|
835
|
+
disabled: !writable || busy,
|
|
836
|
+
onPress: () => void remove(),
|
|
837
|
+
children: "delete"
|
|
838
|
+
})]
|
|
839
|
+
})] })] })
|
|
840
|
+
}) : null
|
|
841
|
+
]
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function fileTab(filePath, relPath) {
|
|
845
|
+
return {
|
|
846
|
+
id: fileTabId(filePath),
|
|
847
|
+
label: relPath.split("/").at(-1) ?? relPath,
|
|
848
|
+
panel: (props) => /* @__PURE__ */ jsx(FilePanel, {
|
|
849
|
+
...props,
|
|
850
|
+
filePath,
|
|
851
|
+
relPath
|
|
852
|
+
})
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
const styles = StyleSheet.create((theme) => ({
|
|
856
|
+
panel: {
|
|
857
|
+
padding: theme.spacing(4),
|
|
858
|
+
gap: theme.spacing(3),
|
|
859
|
+
uni__dependencies: [0]
|
|
860
|
+
},
|
|
861
|
+
header: {
|
|
862
|
+
flexDirection: "row",
|
|
863
|
+
alignItems: "center",
|
|
864
|
+
gap: theme.spacing(2),
|
|
865
|
+
uni__dependencies: [0]
|
|
866
|
+
},
|
|
867
|
+
actions: {
|
|
868
|
+
flexDirection: "row",
|
|
869
|
+
flexWrap: "wrap",
|
|
870
|
+
gap: theme.spacing(2),
|
|
871
|
+
uni__dependencies: [0]
|
|
872
|
+
},
|
|
873
|
+
diff: {
|
|
874
|
+
gap: theme.spacing(2),
|
|
875
|
+
uni__dependencies: [0]
|
|
876
|
+
},
|
|
877
|
+
title: {
|
|
878
|
+
fontSize: theme.fontSize.base,
|
|
879
|
+
fontWeight: "600",
|
|
880
|
+
color: theme.colors.hi,
|
|
881
|
+
uni__dependencies: [0]
|
|
882
|
+
},
|
|
883
|
+
path: {
|
|
884
|
+
flex: 1,
|
|
885
|
+
fontFamily: fontOf(theme.fontFamily.mono),
|
|
886
|
+
fontSize: theme.fontSize.base,
|
|
887
|
+
color: theme.colors.hi,
|
|
888
|
+
uni__dependencies: [0]
|
|
889
|
+
},
|
|
890
|
+
code: {
|
|
891
|
+
fontFamily: fontOf(theme.fontFamily.mono),
|
|
892
|
+
fontSize: theme.fontSize.sm,
|
|
893
|
+
color: theme.colors.text,
|
|
894
|
+
uni__dependencies: [0]
|
|
895
|
+
},
|
|
896
|
+
added: {
|
|
897
|
+
fontFamily: fontOf(theme.fontFamily.mono),
|
|
898
|
+
fontSize: theme.fontSize.sm,
|
|
899
|
+
color: theme.colors.green,
|
|
900
|
+
uni__dependencies: [0]
|
|
901
|
+
},
|
|
902
|
+
removed: {
|
|
903
|
+
fontFamily: fontOf(theme.fontFamily.mono),
|
|
904
|
+
fontSize: theme.fontSize.sm,
|
|
905
|
+
color: theme.colors.red,
|
|
906
|
+
uni__dependencies: [0]
|
|
907
|
+
},
|
|
908
|
+
meta: {
|
|
909
|
+
fontSize: theme.fontSize.sm,
|
|
910
|
+
color: theme.colors.dim,
|
|
911
|
+
uni__dependencies: [0]
|
|
912
|
+
},
|
|
913
|
+
error: {
|
|
914
|
+
fontSize: theme.fontSize.sm,
|
|
915
|
+
color: theme.colors.red,
|
|
916
|
+
uni__dependencies: [0]
|
|
917
|
+
},
|
|
918
|
+
success: {
|
|
919
|
+
fontSize: theme.fontSize.sm,
|
|
920
|
+
color: theme.colors.green,
|
|
921
|
+
uni__dependencies: [0]
|
|
922
|
+
}
|
|
923
|
+
}));
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/_components/FilesActivitySection.native.tsx
|
|
926
|
+
/** The latest changes and a searchable complete picker, backed by the native channel store. */
|
|
927
|
+
function FilesActivitySection({ sessionId, openTransientTab }) {
|
|
928
|
+
const subscribe = useCallback((listener) => {
|
|
929
|
+
const subscription = files.store.subscribe(listener);
|
|
930
|
+
return () => subscription.unsubscribe();
|
|
931
|
+
}, []);
|
|
932
|
+
const { items } = useSyncExternalStore(subscribe, () => files.select(files.store.state, sessionId));
|
|
933
|
+
const [open, setOpen] = useState(false);
|
|
934
|
+
const [query, setQuery] = useState("");
|
|
935
|
+
const choose = (item) => {
|
|
936
|
+
if (sessionId !== null) {
|
|
937
|
+
openTransientTab(fileTab(item.path, item.relPath));
|
|
938
|
+
setOpen(false);
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const row = (item) => /* @__PURE__ */ jsx(ListRow, {
|
|
942
|
+
title: item.relPath,
|
|
943
|
+
description: `${item.tool === "bash" ? "command" : item.tool === "user" ? "you" : item.tool}${item.count > 1 ? ` · ${item.count}×` : ""}${item.diffable ? "" : " · no baseline"}`,
|
|
944
|
+
disabled: sessionId === null,
|
|
945
|
+
onPress: () => choose(item)
|
|
946
|
+
}, item.path);
|
|
947
|
+
if (!items.length) return /* @__PURE__ */ jsx(Text, { children: "nothing changed yet" });
|
|
948
|
+
return /* @__PURE__ */ jsxs(View, { children: [
|
|
949
|
+
items.slice(0, 5).map(row),
|
|
950
|
+
items.length > 5 ? /* @__PURE__ */ jsxs(Button, {
|
|
951
|
+
variant: "ghost",
|
|
952
|
+
onPress: () => setOpen(true),
|
|
953
|
+
children: [
|
|
954
|
+
"show all ",
|
|
955
|
+
items.length,
|
|
956
|
+
" files"
|
|
957
|
+
]
|
|
958
|
+
}) : null,
|
|
959
|
+
open ? /* @__PURE__ */ jsx(Sheet, {
|
|
960
|
+
open: true,
|
|
961
|
+
onOpenChange: setOpen,
|
|
962
|
+
children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "changed files" }) }), /* @__PURE__ */ jsxs(SheetBody, { children: [/* @__PURE__ */ jsx(Input, {
|
|
963
|
+
accessibilityLabel: "search changed file paths",
|
|
964
|
+
value: query,
|
|
965
|
+
onChangeText: setQuery,
|
|
966
|
+
placeholder: "search paths…"
|
|
967
|
+
}), items.filter((item) => item.relPath.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())).map(row)] })] })
|
|
968
|
+
}) : null
|
|
969
|
+
] });
|
|
970
|
+
}
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/Files.activity.files.native.tsx
|
|
973
|
+
var Files_activity_files_native_default = defineFill({
|
|
974
|
+
slot: "activity.files",
|
|
975
|
+
id: "files",
|
|
976
|
+
component: FilesActivitySection
|
|
977
|
+
});
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region generated/native.ts
|
|
980
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
981
|
+
...identity,
|
|
982
|
+
...value
|
|
983
|
+
};
|
|
984
|
+
const nativePlugin = defineNativePlugin({
|
|
985
|
+
id: "files",
|
|
986
|
+
session: {
|
|
987
|
+
get activityGroups() {
|
|
988
|
+
return [via({ name: "files" }, files_native_default)];
|
|
989
|
+
},
|
|
990
|
+
get channels() {
|
|
991
|
+
return [via({ channel: "file_edits" }, file_edits_native_default)];
|
|
992
|
+
},
|
|
993
|
+
get fills() {
|
|
994
|
+
return [via({
|
|
995
|
+
slot: "activity.files",
|
|
996
|
+
id: "files"
|
|
997
|
+
}, Files_activity_files_native_default)];
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
//#endregion
|
|
1002
|
+
export { nativePlugin };
|
|
1003
|
+
|
|
1004
|
+
//# sourceMappingURL=native.mjs.map
|