@agimon-ai/po-file-edit 0.0.0-alpha.0
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/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/apiContracts.cjs +156 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +151 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/extensions/server.cjs +1934 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +1903 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +1344 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index.cjs +0 -0
- package/dist/index.d.mts +91 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +129 -0
|
@@ -0,0 +1,1344 @@
|
|
|
1
|
+
import { apiResponse, createApiClient, defineActivityGroup, defineApiRoutes, defineChannelFile, defineFileLinks, defineFill, defineLeaderBinding, defineSessionStore, defineWebPlugin, sessionApiAddress } from "@agimon-ai/po-core/web";
|
|
2
|
+
import { Badge, Breadcrumb, Button, CloseIcon, CodeEditor, Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DiffView, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Input, KebabIcon, Markdown, MediaPreview, ReviewCommentDraft, buildReviewPrompt, grammarKeyOf, groupByDirectory, groupRowLabel, mediaKindOf, reviewCommentAnchor } from "@agimon-ai/po-web-components";
|
|
3
|
+
import { useStore } from "@tanstack/react-store";
|
|
4
|
+
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { sealedTransport } from "@agimon-ai/po-web-security/browser";
|
|
6
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/types/webFiles.ts
|
|
8
|
+
/** The frame type the hub publishes and the web plugin claims; declared in the poWeb block too. */
|
|
9
|
+
const filesChannelType = "file_edits";
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/extensions/workspaces/sessions/(frontend)/activity-group/files.web.ts
|
|
12
|
+
var files_web_default$2 = defineActivityGroup({
|
|
13
|
+
keys: "e f",
|
|
14
|
+
statusKey: "doom-file-edit-files",
|
|
15
|
+
hideWhenEmpty: true,
|
|
16
|
+
marksBackgroundWork: false,
|
|
17
|
+
order: 15
|
|
18
|
+
});
|
|
19
|
+
const files = defineSessionStore({
|
|
20
|
+
items: [],
|
|
21
|
+
detail: {},
|
|
22
|
+
loading: [],
|
|
23
|
+
errors: {},
|
|
24
|
+
comments: []
|
|
25
|
+
});
|
|
26
|
+
function isRecord$1(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function isItem(value) {
|
|
30
|
+
return isRecord$1(value) && typeof value.path === "string" && typeof value.relPath === "string" && typeof value.tool === "string" && typeof value.at === "number" && typeof value.count === "number";
|
|
31
|
+
}
|
|
32
|
+
/** The plugin's session data channel: 'file_edits' payloads into the store. */
|
|
33
|
+
const filesChannel = files.channel({
|
|
34
|
+
channel: filesChannelType,
|
|
35
|
+
parse(input) {
|
|
36
|
+
if (!isRecord$1(input) || !Array.isArray(input.items)) return null;
|
|
37
|
+
return { items: input.items.filter(isItem) };
|
|
38
|
+
},
|
|
39
|
+
reduce: (current, { items }) => ({
|
|
40
|
+
...current,
|
|
41
|
+
items
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
function markLoading(sessionId, filePath) {
|
|
45
|
+
files.update(sessionId, (current) => current.loading.includes(filePath) ? current : {
|
|
46
|
+
...current,
|
|
47
|
+
loading: [...current.loading, filePath],
|
|
48
|
+
errors: withoutKey(current.errors, filePath)
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function storeDetail(sessionId, detail) {
|
|
52
|
+
files.update(sessionId, (current) => ({
|
|
53
|
+
...current,
|
|
54
|
+
detail: {
|
|
55
|
+
...current.detail,
|
|
56
|
+
[detail.path]: detail
|
|
57
|
+
},
|
|
58
|
+
loading: current.loading.filter((path) => path !== detail.path),
|
|
59
|
+
errors: withoutKey(current.errors, detail.path)
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
function storeError(sessionId, filePath, message) {
|
|
63
|
+
files.update(sessionId, (current) => ({
|
|
64
|
+
...current,
|
|
65
|
+
loading: current.loading.filter((path) => path !== filePath),
|
|
66
|
+
errors: {
|
|
67
|
+
...current.errors,
|
|
68
|
+
[filePath]: message
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
function addComment(sessionId, comment) {
|
|
73
|
+
files.update(sessionId, (current) => ({
|
|
74
|
+
...current,
|
|
75
|
+
comments: [...current.comments, comment]
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
function removeComment(sessionId, id) {
|
|
79
|
+
files.update(sessionId, (current) => ({
|
|
80
|
+
...current,
|
|
81
|
+
comments: current.comments.filter((comment) => comment.id !== id)
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
function clearComments(sessionId, filePath) {
|
|
85
|
+
files.update(sessionId, (current) => ({
|
|
86
|
+
...current,
|
|
87
|
+
comments: current.comments.filter((comment) => comment.path !== filePath)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
function withoutKey(source, key) {
|
|
91
|
+
return Object.fromEntries(Object.entries(source).filter(([entry]) => entry !== key));
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/file-edits.web.ts
|
|
95
|
+
var file_edits_web_default = defineChannelFile(filesChannel);
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/types/apiRoutes.ts
|
|
98
|
+
/**
|
|
99
|
+
* This package's routes, as data.
|
|
100
|
+
*
|
|
101
|
+
* No scope and no base path. The build reads both off
|
|
102
|
+
* `src/extensions/workspaces/sessions/(backend)/api/file-edits/`, so the mount
|
|
103
|
+
* is stated once, by the folder that creates it. They used to be written here,
|
|
104
|
+
* in the contract and again inside every URL the page built, and the copies
|
|
105
|
+
* were free to disagree.
|
|
106
|
+
*
|
|
107
|
+
* This is the one file the routes, the Hono app and the browser all read, so a
|
|
108
|
+
* path cannot move on one side alone.
|
|
109
|
+
*/
|
|
110
|
+
var apiRoutes_default = defineApiRoutes({
|
|
111
|
+
detail: {
|
|
112
|
+
method: "GET",
|
|
113
|
+
path: "/detail",
|
|
114
|
+
query: ["path"],
|
|
115
|
+
response: apiResponse()
|
|
116
|
+
},
|
|
117
|
+
preview: {
|
|
118
|
+
method: "GET",
|
|
119
|
+
path: "/preview",
|
|
120
|
+
query: ["path"],
|
|
121
|
+
response: apiResponse()
|
|
122
|
+
},
|
|
123
|
+
save: {
|
|
124
|
+
method: "PUT",
|
|
125
|
+
path: "/content",
|
|
126
|
+
response: apiResponse()
|
|
127
|
+
},
|
|
128
|
+
/** The same path as `save`; the method is the whole of the difference. */
|
|
129
|
+
remove: {
|
|
130
|
+
method: "DELETE",
|
|
131
|
+
path: "/content",
|
|
132
|
+
query: ["path"]
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* The host's own bytes route, not this package's.
|
|
136
|
+
*
|
|
137
|
+
* `host: true` hangs it off the session root rather than under `/plugins`,
|
|
138
|
+
* which is how the cockpit already serves it. Declaring it here is the first
|
|
139
|
+
* time anything records that this package's previews depend on it.
|
|
140
|
+
*/
|
|
141
|
+
file: {
|
|
142
|
+
method: "GET",
|
|
143
|
+
path: "/file",
|
|
144
|
+
query: ["path"],
|
|
145
|
+
host: true
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const api = createApiClient(apiRoutes_default, {
|
|
149
|
+
scopes: ["session"],
|
|
150
|
+
basePath: "file-edits",
|
|
151
|
+
transport: (input, init) => sealedTransport.fetch(input, init),
|
|
152
|
+
sessionAddress: sessionApiAddress
|
|
153
|
+
});
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/types/fileEditsApi.ts
|
|
156
|
+
/** Query parameter naming the file a request is about. */
|
|
157
|
+
const PATH_QUERY_PARAM = "path";
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/filesApi.ts
|
|
160
|
+
/**
|
|
161
|
+
* The page's half of this package's session API: one file's history, the
|
|
162
|
+
* manual save, and the deletion.
|
|
163
|
+
*
|
|
164
|
+
* These are adapters now, not a transport. The generated client owns the URL
|
|
165
|
+
* and the sealed transport with it, so nothing here spells a route or reaches
|
|
166
|
+
* for `fetch`. What stays is this package's own vocabulary: which refusals the
|
|
167
|
+
* reader is shown, and the stale-save contract the editor depends on.
|
|
168
|
+
*
|
|
169
|
+
* Sealing still matters and still happens, one layer down. What travels here is
|
|
170
|
+
* file contents in both directions, which is the worst thing in the cockpit to
|
|
171
|
+
* hand a tunnel's relay in the clear.
|
|
172
|
+
*/
|
|
173
|
+
const UNREACHABLE = "The session is unreachable.";
|
|
174
|
+
function isRecord(value) {
|
|
175
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Where a file's raw bytes are served from, for the ones the page shows rather
|
|
179
|
+
* than edits.
|
|
180
|
+
*
|
|
181
|
+
* This is the host's route, not this package's, which is why the route table
|
|
182
|
+
* marks it `host: true` and it lands beside the session rather than under
|
|
183
|
+
* `/plugins`. The cockpit already serves any file under a session's working
|
|
184
|
+
* directory, and that route carries the traversal guard, the size cap and the
|
|
185
|
+
* content-type mapping. A second one here would duplicate a security boundary
|
|
186
|
+
* to avoid duplicating a URL, which is the wrong way round.
|
|
187
|
+
*/
|
|
188
|
+
function sessionFileUrl(sessionId, relPath) {
|
|
189
|
+
return api.session(sessionId).file.url({ query: { [PATH_QUERY_PARAM]: relPath } });
|
|
190
|
+
}
|
|
191
|
+
/** The message a reader is shown: the route's own words, or why there were none. */
|
|
192
|
+
function messageOf(result) {
|
|
193
|
+
if (result.status === 0) return UNREACHABLE;
|
|
194
|
+
return result.error === "" ? `The session answered ${result.status}.` : result.error;
|
|
195
|
+
}
|
|
196
|
+
async function fetchFileDetail(sessionId, filePath) {
|
|
197
|
+
const result = await api.session(sessionId).detail({ query: { [PATH_QUERY_PARAM]: filePath } });
|
|
198
|
+
if (!result.ok) return {
|
|
199
|
+
ok: false,
|
|
200
|
+
error: messageOf(result)
|
|
201
|
+
};
|
|
202
|
+
if (!isRecord(result.data)) return {
|
|
203
|
+
ok: false,
|
|
204
|
+
error: "The session answered with no detail."
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
ok: true,
|
|
208
|
+
detail: result.data
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** One file the session never changed, read only; the route bounds it to the working directory. */
|
|
212
|
+
async function fetchFilePreview(sessionId, filePath) {
|
|
213
|
+
const result = await api.session(sessionId).preview({ query: { [PATH_QUERY_PARAM]: filePath } });
|
|
214
|
+
if (!result.ok) return {
|
|
215
|
+
ok: false,
|
|
216
|
+
error: messageOf(result)
|
|
217
|
+
};
|
|
218
|
+
if (!isRecord(result.data)) return {
|
|
219
|
+
ok: false,
|
|
220
|
+
error: "The session answered with no file."
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
ok: true,
|
|
224
|
+
preview: result.data
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function saveFileContent(sessionId, filePath, expectedHash, content) {
|
|
228
|
+
const result = await api.session(sessionId).save({ body: {
|
|
229
|
+
path: filePath,
|
|
230
|
+
expectedHash,
|
|
231
|
+
content
|
|
232
|
+
} });
|
|
233
|
+
if (result.status === 409) {
|
|
234
|
+
const data = result.ok ? void 0 : result.data;
|
|
235
|
+
const hash = isRecord(data) && typeof data.hash === "string" ? data.hash : void 0;
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
stale: true,
|
|
239
|
+
error: result.ok ? "The file changed since it was opened." : messageOf(result),
|
|
240
|
+
...hash === void 0 ? {} : { hash }
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (!result.ok) return {
|
|
244
|
+
ok: false,
|
|
245
|
+
stale: false,
|
|
246
|
+
error: messageOf(result)
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
hash: isRecord(result.data) && typeof result.data.hash === "string" ? result.data.hash : ""
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Removes the file from disk.
|
|
255
|
+
*
|
|
256
|
+
* The route takes only a path, with no hash to prove the reader saw what is
|
|
257
|
+
* there now: a deletion discards the file whatever it holds, so a staleness
|
|
258
|
+
* check would guard nothing. The confirmation in front of it is the guard.
|
|
259
|
+
*/
|
|
260
|
+
async function deleteFile(sessionId, filePath) {
|
|
261
|
+
const result = await api.session(sessionId).remove({ query: { [PATH_QUERY_PARAM]: filePath } });
|
|
262
|
+
return result.ok ? { ok: true } : {
|
|
263
|
+
ok: false,
|
|
264
|
+
error: messageOf(result)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/fileView.ts
|
|
269
|
+
/**
|
|
270
|
+
* Pure view logic the file surfaces share: how a change is labelled, how a
|
|
271
|
+
* file is identified, and how a review reaches the agent.
|
|
272
|
+
*
|
|
273
|
+
* None of this touches React or the DOM beyond what it is handed, so the
|
|
274
|
+
* awkward parts are pinned by tests rather than by clicking through a browser.
|
|
275
|
+
* Wording a review and drawing a diff are shared with other review surfaces
|
|
276
|
+
* and live in po-web-components.
|
|
277
|
+
*/
|
|
278
|
+
/** What a row says about the tool behind a change. */
|
|
279
|
+
const TOOL_LABEL = {
|
|
280
|
+
edit: "edit",
|
|
281
|
+
write: "write",
|
|
282
|
+
bash: "command",
|
|
283
|
+
user: "you"
|
|
284
|
+
};
|
|
285
|
+
/** Filters visible relative paths without changing the timeline's newest-first order. */
|
|
286
|
+
function filterFileItems(items, query) {
|
|
287
|
+
const normalized = query.trim().toLocaleLowerCase();
|
|
288
|
+
if (normalized.length === 0) return [...items];
|
|
289
|
+
return items.filter((item) => item.relPath.toLocaleLowerCase().includes(normalized));
|
|
290
|
+
}
|
|
291
|
+
function previewModeOf(filePath, unavailable) {
|
|
292
|
+
if (mediaKindOf(filePath) !== "download") return "media";
|
|
293
|
+
if (unavailable) return "unavailable";
|
|
294
|
+
const lower = filePath.toLowerCase();
|
|
295
|
+
if (lower.endsWith(".md") || lower.endsWith(".markdown")) return "markdown";
|
|
296
|
+
if (lower.endsWith(".html") || lower.endsWith(".htm")) return "html";
|
|
297
|
+
return grammarKeyOf(filePath) === void 0 ? "text" : "code";
|
|
298
|
+
}
|
|
299
|
+
/** A short, stable fingerprint of a string; enough to separate two paths in an id. */
|
|
300
|
+
function fingerprint(value) {
|
|
301
|
+
let hash = 2166136261;
|
|
302
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
303
|
+
hash ^= value.charCodeAt(index);
|
|
304
|
+
hash = Math.imul(hash, 16777619);
|
|
305
|
+
}
|
|
306
|
+
return (hash >>> 0).toString(36);
|
|
307
|
+
}
|
|
308
|
+
/** The transient tab id for one file; stable, so reopening focuses rather than duplicates. */
|
|
309
|
+
function fileTabId(filePath) {
|
|
310
|
+
const slug = filePath.replaceAll(/[^a-zA-Z0-9]+/gu, "-").replace(/^-|-$/gu, "");
|
|
311
|
+
return `files-file-${fingerprint(filePath)}-${slug.slice(-40)}`;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Sends a review as a deferred follow-up and reports whether it left.
|
|
315
|
+
*
|
|
316
|
+
* `follow_up` waits behind a running turn and starts when idle, without
|
|
317
|
+
* treating the review as an explicit Queue action. The sender throws when
|
|
318
|
+
* disconnected, so the caller keeps the comments and reports the reason.
|
|
319
|
+
*/
|
|
320
|
+
function sendReviewFrame(send, comments) {
|
|
321
|
+
try {
|
|
322
|
+
send({
|
|
323
|
+
type: "follow_up",
|
|
324
|
+
message: buildReviewPrompt(comments)
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return error instanceof Error ? error.message : String(error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/DeleteFileDialog.tsx
|
|
333
|
+
function DeleteFileDialog({ relPath, open, onConfirm, onCancel }) {
|
|
334
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
335
|
+
open,
|
|
336
|
+
onOpenChange: (next) => {
|
|
337
|
+
if (!next) onCancel();
|
|
338
|
+
},
|
|
339
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
340
|
+
width: "sm",
|
|
341
|
+
"data-testid": "files-delete-dialog",
|
|
342
|
+
"aria-describedby": void 0,
|
|
343
|
+
children: [/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: "delete file" }) }), /* @__PURE__ */ jsxs(DialogBody, { children: [/* @__PURE__ */ jsxs(DialogDescription, { children: [
|
|
344
|
+
"this removes ",
|
|
345
|
+
/* @__PURE__ */ jsx("span", {
|
|
346
|
+
className: "font-semibold text-doom-hi",
|
|
347
|
+
children: relPath
|
|
348
|
+
}),
|
|
349
|
+
" from disk. the session keeps its record of the change, but the file itself is gone."
|
|
350
|
+
] }), /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
351
|
+
variant: "outline",
|
|
352
|
+
size: "md",
|
|
353
|
+
"data-testid": "files-delete-cancel",
|
|
354
|
+
autoFocus: true,
|
|
355
|
+
onClick: onCancel,
|
|
356
|
+
children: "cancel"
|
|
357
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
358
|
+
variant: "danger",
|
|
359
|
+
size: "md",
|
|
360
|
+
"data-testid": "files-delete-confirm",
|
|
361
|
+
onClick: onConfirm,
|
|
362
|
+
children: "delete"
|
|
363
|
+
})] })] })]
|
|
364
|
+
})
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/SessionMediaPreview.tsx
|
|
369
|
+
/** Media elements cannot issue the encrypted fetch required by remote session files. */
|
|
370
|
+
function SessionMediaPreview({ src, path, "data-testid": testId }) {
|
|
371
|
+
const [loaded, setLoaded] = useState();
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (src === "") return;
|
|
374
|
+
const controller = new AbortController();
|
|
375
|
+
let url;
|
|
376
|
+
(async () => {
|
|
377
|
+
const response = await sealedTransport.fetch(src, {
|
|
378
|
+
signal: controller.signal,
|
|
379
|
+
cache: "no-store"
|
|
380
|
+
});
|
|
381
|
+
if (!response.ok) throw new Error(`Media could not be loaded (${response.status}).`);
|
|
382
|
+
const blob = await response.blob();
|
|
383
|
+
if (controller.signal.aborted) return;
|
|
384
|
+
url = URL.createObjectURL(blob);
|
|
385
|
+
setLoaded({
|
|
386
|
+
source: src,
|
|
387
|
+
url,
|
|
388
|
+
contentType: (response.headers.get("content-type") ?? blob.type).split(";", 1)[0]?.trim().toLowerCase() ?? ""
|
|
389
|
+
});
|
|
390
|
+
})().catch((error) => {
|
|
391
|
+
if (!controller.signal.aborted) setLoaded({
|
|
392
|
+
source: src,
|
|
393
|
+
error: error instanceof Error ? error.message : String(error)
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
return () => {
|
|
397
|
+
controller.abort();
|
|
398
|
+
if (url !== void 0) URL.revokeObjectURL(url);
|
|
399
|
+
};
|
|
400
|
+
}, [src]);
|
|
401
|
+
const current = loaded?.source === src ? loaded : void 0;
|
|
402
|
+
if (current?.url === void 0) return /* @__PURE__ */ jsx("p", {
|
|
403
|
+
role: current?.error ? "alert" : "status",
|
|
404
|
+
children: current?.error ?? "Loading media..."
|
|
405
|
+
});
|
|
406
|
+
return /* @__PURE__ */ jsx(MediaPreview, {
|
|
407
|
+
src: current.url,
|
|
408
|
+
path,
|
|
409
|
+
contentType: current.contentType,
|
|
410
|
+
"data-testid": testId
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/** Resolves markdown images beside their document, leaving containment to the host. */
|
|
414
|
+
async function loadMarkdownImage(sessionId, documentPath, path) {
|
|
415
|
+
if (sessionId === null) throw new Error("The session is unavailable.");
|
|
416
|
+
const directory = documentPath.slice(0, documentPath.lastIndexOf("/") + 1);
|
|
417
|
+
const response = await sealedTransport.fetch(sessionFileUrl(sessionId, path.startsWith("/") ? path : `${directory}${path}`), { cache: "no-store" });
|
|
418
|
+
if (!response.ok) throw new Error(`Image could not be loaded (${response.status}).`);
|
|
419
|
+
const url = URL.createObjectURL(await response.blob());
|
|
420
|
+
return {
|
|
421
|
+
url,
|
|
422
|
+
dispose: () => URL.revokeObjectURL(url)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/FilePanel.tsx
|
|
427
|
+
/** How a version heads its row: what ran, when, and what it moved. */
|
|
428
|
+
function versionSummary(version) {
|
|
429
|
+
const time = new Date(version.at).toLocaleTimeString();
|
|
430
|
+
const counts = version.hunks === void 0 ? "" : ` +${version.additions} -${version.removals}`;
|
|
431
|
+
return `${time} · ${TOOL_LABEL[version.tool] ?? version.tool}${counts}`;
|
|
432
|
+
}
|
|
433
|
+
function FilePanel({ filePath, relPath, sessionId, sendSessionFrame, closeTransientTab }) {
|
|
434
|
+
const session = useStore(files.store, (state) => files.select(state, sessionId));
|
|
435
|
+
const detail = session.detail[filePath];
|
|
436
|
+
const error = session.errors[filePath];
|
|
437
|
+
const loading = session.loading.includes(filePath);
|
|
438
|
+
const comments = session.comments.filter((comment) => comment.path === filePath);
|
|
439
|
+
const [view, setView] = useState("preview");
|
|
440
|
+
const [draft, setDraft] = useState(void 0);
|
|
441
|
+
const [source, setSource] = useState(void 0);
|
|
442
|
+
const [expanded, setExpanded] = useState(void 0);
|
|
443
|
+
const [saving, setSaving] = useState(false);
|
|
444
|
+
const [saveNote, setSaveNote] = useState(void 0);
|
|
445
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
446
|
+
const [reviewError, setReviewError] = useState(void 0);
|
|
447
|
+
const selection = useRef(void 0);
|
|
448
|
+
const revision = session.items.find((item) => item.path === filePath)?.count ?? 0;
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (sessionId === null) return;
|
|
451
|
+
let cancelled = false;
|
|
452
|
+
markLoading(sessionId, filePath);
|
|
453
|
+
fetchFileDetail(sessionId, filePath).then((result) => {
|
|
454
|
+
if (cancelled) return;
|
|
455
|
+
if (result.ok) {
|
|
456
|
+
storeDetail(sessionId, result.detail);
|
|
457
|
+
setSource(void 0);
|
|
458
|
+
setSaveNote(void 0);
|
|
459
|
+
} else storeError(sessionId, filePath, result.error);
|
|
460
|
+
});
|
|
461
|
+
return () => {
|
|
462
|
+
cancelled = true;
|
|
463
|
+
};
|
|
464
|
+
}, [
|
|
465
|
+
sessionId,
|
|
466
|
+
filePath,
|
|
467
|
+
revision
|
|
468
|
+
]);
|
|
469
|
+
const working = detail?.working;
|
|
470
|
+
const outside = relPath === ".." || relPath.startsWith("../") || relPath.startsWith("/");
|
|
471
|
+
const content = source ?? working?.content ?? "";
|
|
472
|
+
const previewMode = previewModeOf(relPath, working?.unavailable === true);
|
|
473
|
+
const loadImage = useCallback((path) => loadMarkdownImage(sessionId, relPath, path), [sessionId, relPath]);
|
|
474
|
+
const mediaSrc = sessionId === null ? "" : sessionFileUrl(sessionId, relPath);
|
|
475
|
+
const openDraft = (selection) => {
|
|
476
|
+
setDraft(selection);
|
|
477
|
+
};
|
|
478
|
+
/** Raises the comment box on whatever the editor last reported, if that was a selection. */
|
|
479
|
+
const openSelectionDraft = () => {
|
|
480
|
+
const range = selection.current;
|
|
481
|
+
if (range === void 0 || range.text.trim() === "") return;
|
|
482
|
+
openDraft({
|
|
483
|
+
snippet: range.text,
|
|
484
|
+
startLine: range.startLine,
|
|
485
|
+
endLine: range.endLine
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
const submitDraft = (body) => {
|
|
489
|
+
if (sessionId === null || draft === void 0) return;
|
|
490
|
+
addComment(sessionId, {
|
|
491
|
+
id: `${filePath}:${draft.side ?? "new"}:${draft.startLine ?? 0}:${comments.length}:${body.length}`,
|
|
492
|
+
path: filePath,
|
|
493
|
+
relPath,
|
|
494
|
+
...draft.side === void 0 ? {} : { side: draft.side },
|
|
495
|
+
...draft.startLine === void 0 ? {} : { startLine: draft.startLine },
|
|
496
|
+
...draft.endLine === void 0 ? {} : { endLine: draft.endLine },
|
|
497
|
+
snippet: draft.snippet,
|
|
498
|
+
body
|
|
499
|
+
});
|
|
500
|
+
setDraft(void 0);
|
|
501
|
+
};
|
|
502
|
+
const sendReview = () => {
|
|
503
|
+
if (sessionId === null || comments.length === 0) return;
|
|
504
|
+
const failure = sendReviewFrame((frame) => sendSessionFrame(sessionId, frame), comments);
|
|
505
|
+
setReviewError(failure);
|
|
506
|
+
if (failure === void 0) clearComments(sessionId, filePath);
|
|
507
|
+
};
|
|
508
|
+
const confirmDelete = async () => {
|
|
509
|
+
if (sessionId === null) return;
|
|
510
|
+
setConfirmingDelete(false);
|
|
511
|
+
const result = await deleteFile(sessionId, filePath);
|
|
512
|
+
if (!result.ok) {
|
|
513
|
+
storeError(sessionId, filePath, result.error);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
closeTransientTab(fileTabId(filePath));
|
|
517
|
+
};
|
|
518
|
+
const save = async () => {
|
|
519
|
+
if (sessionId === null || working === void 0 || source === void 0) return;
|
|
520
|
+
setSaving(true);
|
|
521
|
+
const result = await saveFileContent(sessionId, filePath, working.hash, source);
|
|
522
|
+
setSaving(false);
|
|
523
|
+
if (result.ok) {
|
|
524
|
+
setSaveNote("saved");
|
|
525
|
+
setSource(void 0);
|
|
526
|
+
markLoading(sessionId, filePath);
|
|
527
|
+
const refreshed = await fetchFileDetail(sessionId, filePath);
|
|
528
|
+
if (refreshed.ok) storeDetail(sessionId, refreshed.detail);
|
|
529
|
+
else storeError(sessionId, filePath, refreshed.error);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
setSaveNote(result.error);
|
|
533
|
+
};
|
|
534
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
535
|
+
"data-testid": "files-file-panel",
|
|
536
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
537
|
+
children: [
|
|
538
|
+
/* @__PURE__ */ jsxs("header", {
|
|
539
|
+
className: "flex shrink-0 items-center gap-2 border-b border-doom-border px-4 py-2",
|
|
540
|
+
children: [
|
|
541
|
+
/* @__PURE__ */ jsx(Breadcrumb, {
|
|
542
|
+
path: relPath,
|
|
543
|
+
"data-testid": "files-breadcrumb",
|
|
544
|
+
className: "min-w-0 flex-1"
|
|
545
|
+
}),
|
|
546
|
+
view === "edit" ? /* @__PURE__ */ jsx(Badge, {
|
|
547
|
+
tone: "neutral",
|
|
548
|
+
"data-testid": "files-editing",
|
|
549
|
+
className: "shrink-0 ",
|
|
550
|
+
children: "editing"
|
|
551
|
+
}) : null,
|
|
552
|
+
detail === void 0 ? null : /* @__PURE__ */ jsxs("span", {
|
|
553
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
554
|
+
children: [
|
|
555
|
+
"+",
|
|
556
|
+
detail.cumulative.additions,
|
|
557
|
+
" -",
|
|
558
|
+
detail.cumulative.removals
|
|
559
|
+
]
|
|
560
|
+
}),
|
|
561
|
+
/* @__PURE__ */ jsx("nav", {
|
|
562
|
+
className: "flex shrink-0 items-center gap-1",
|
|
563
|
+
children: ["preview", "diff"].map((entry) => /* @__PURE__ */ jsx(Button, {
|
|
564
|
+
variant: view === entry ? "outline" : "ghost",
|
|
565
|
+
size: "xs",
|
|
566
|
+
"data-testid": `files-view-${entry}`,
|
|
567
|
+
onClick: () => setView(entry),
|
|
568
|
+
className: "",
|
|
569
|
+
children: entry
|
|
570
|
+
}, entry))
|
|
571
|
+
}),
|
|
572
|
+
/* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
573
|
+
asChild: true,
|
|
574
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
575
|
+
variant: "ghost",
|
|
576
|
+
size: "icon",
|
|
577
|
+
"data-testid": "files-menu",
|
|
578
|
+
title: "file actions",
|
|
579
|
+
className: "shrink-0 text-doom-faint hover:text-doom-hi",
|
|
580
|
+
children: /* @__PURE__ */ jsx(KebabIcon, { className: "h-3 w-3" })
|
|
581
|
+
})
|
|
582
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
583
|
+
"data-testid": "files-menu-list",
|
|
584
|
+
children: [
|
|
585
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
586
|
+
"data-testid": "files-edit",
|
|
587
|
+
disabled: outside || working?.unavailable === true,
|
|
588
|
+
onSelect: () => setView("edit"),
|
|
589
|
+
children: "edit"
|
|
590
|
+
}),
|
|
591
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
592
|
+
variant: "destructive",
|
|
593
|
+
"data-testid": "files-delete",
|
|
594
|
+
disabled: outside || working?.unavailable === true,
|
|
595
|
+
onSelect: () => setConfirmingDelete(true),
|
|
596
|
+
children: "delete"
|
|
597
|
+
}),
|
|
598
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
599
|
+
"data-testid": "files-close",
|
|
600
|
+
onSelect: () => closeTransientTab(fileTabId(filePath)),
|
|
601
|
+
children: "close"
|
|
602
|
+
})
|
|
603
|
+
]
|
|
604
|
+
})] })
|
|
605
|
+
]
|
|
606
|
+
}),
|
|
607
|
+
/* @__PURE__ */ jsx(DeleteFileDialog, {
|
|
608
|
+
relPath,
|
|
609
|
+
open: confirmingDelete,
|
|
610
|
+
onConfirm: () => void confirmDelete(),
|
|
611
|
+
onCancel: () => setConfirmingDelete(false)
|
|
612
|
+
}),
|
|
613
|
+
loading && detail === void 0 ? /* @__PURE__ */ jsx("p", {
|
|
614
|
+
"data-testid": "files-loading",
|
|
615
|
+
className: "px-4 py-3 text-sm text-doom-dim",
|
|
616
|
+
children: "reading this file's history…"
|
|
617
|
+
}) : null,
|
|
618
|
+
error === void 0 ? null : /* @__PURE__ */ jsx("p", {
|
|
619
|
+
"data-testid": "files-error",
|
|
620
|
+
className: "px-4 py-3 text-sm text-doom-red",
|
|
621
|
+
children: error
|
|
622
|
+
}),
|
|
623
|
+
/* @__PURE__ */ jsx("div", {
|
|
624
|
+
className: "min-h-0 flex-1 overflow-y-auto",
|
|
625
|
+
children: detail === void 0 ? null : view === "diff" ? /* @__PURE__ */ jsxs("div", {
|
|
626
|
+
className: "flex flex-col",
|
|
627
|
+
children: [
|
|
628
|
+
detail.cumulative.note === void 0 ? null : /* @__PURE__ */ jsx("p", {
|
|
629
|
+
"data-testid": "files-cumulative-note",
|
|
630
|
+
className: "px-4 py-2 text-sm text-doom-yellow",
|
|
631
|
+
children: detail.cumulative.note
|
|
632
|
+
}),
|
|
633
|
+
detail.cumulative.hunks === void 0 ? null : /* @__PURE__ */ jsx(DiffView, {
|
|
634
|
+
hunks: detail.cumulative.hunks,
|
|
635
|
+
testId: "files-cumulative-diff",
|
|
636
|
+
onSelect: (selection) => openDraft(selection)
|
|
637
|
+
}),
|
|
638
|
+
/* @__PURE__ */ jsxs("section", {
|
|
639
|
+
className: "mt-2 border-t border-doom-border",
|
|
640
|
+
children: [/* @__PURE__ */ jsxs("h2", {
|
|
641
|
+
className: "px-4 py-2 text-lg font-semibold text-doom-hi ",
|
|
642
|
+
children: [
|
|
643
|
+
detail.versions.length,
|
|
644
|
+
" ",
|
|
645
|
+
detail.versions.length === 1 ? "change" : "changes",
|
|
646
|
+
" this session"
|
|
647
|
+
]
|
|
648
|
+
}), detail.versions.map((version) => /* @__PURE__ */ jsxs("article", {
|
|
649
|
+
"data-testid": `files-version-${version.index}`,
|
|
650
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
651
|
+
variant: "ghost",
|
|
652
|
+
size: "card",
|
|
653
|
+
"data-testid": `files-version-toggle-${version.index}`,
|
|
654
|
+
onClick: () => setExpanded(expanded === version.index ? void 0 : version.index),
|
|
655
|
+
className: "w-full gap-2 rounded-none px-4 py-1.5 hover:bg-doom-panel",
|
|
656
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
657
|
+
className: "flex w-full min-w-0 items-center gap-2",
|
|
658
|
+
children: [
|
|
659
|
+
/* @__PURE__ */ jsxs("span", {
|
|
660
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
661
|
+
children: ["#", version.index]
|
|
662
|
+
}),
|
|
663
|
+
/* @__PURE__ */ jsx("span", {
|
|
664
|
+
className: "min-w-0 flex-1 truncate text-left text-base text-doom-text",
|
|
665
|
+
children: versionSummary(version)
|
|
666
|
+
}),
|
|
667
|
+
version.origin === "scan" ? /* @__PURE__ */ jsx(Badge, {
|
|
668
|
+
tone: "neutral",
|
|
669
|
+
className: "shrink-0 ",
|
|
670
|
+
children: "no baseline"
|
|
671
|
+
}) : null,
|
|
672
|
+
/* @__PURE__ */ jsx("span", {
|
|
673
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
674
|
+
children: expanded === version.index ? "▾" : "▸"
|
|
675
|
+
})
|
|
676
|
+
]
|
|
677
|
+
})
|
|
678
|
+
}), expanded !== version.index ? null : version.hunks === void 0 ? /* @__PURE__ */ jsx("p", {
|
|
679
|
+
className: "px-4 py-1.5 text-sm text-doom-dim",
|
|
680
|
+
children: version.note ?? "nothing to show"
|
|
681
|
+
}) : /* @__PURE__ */ jsx(DiffView, {
|
|
682
|
+
hunks: version.hunks,
|
|
683
|
+
testId: `files-version-diff-${version.index}`,
|
|
684
|
+
onSelect: (selection) => openDraft(selection)
|
|
685
|
+
})]
|
|
686
|
+
}, version.index))]
|
|
687
|
+
})
|
|
688
|
+
]
|
|
689
|
+
}) : view === "edit" ? /* @__PURE__ */ jsx("div", {
|
|
690
|
+
className: "flex h-full flex-col gap-2 p-3",
|
|
691
|
+
children: working?.unavailable === true ? /* @__PURE__ */ jsx("p", {
|
|
692
|
+
"data-testid": "files-source-unavailable",
|
|
693
|
+
className: "text-sm text-doom-dim",
|
|
694
|
+
children: working.reason ?? "this file cannot be shown"
|
|
695
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
|
|
696
|
+
className: "min-h-[24rem] flex-1",
|
|
697
|
+
onMouseUp: openSelectionDraft,
|
|
698
|
+
onKeyUp: (event) => {
|
|
699
|
+
if (event.shiftKey) openSelectionDraft();
|
|
700
|
+
},
|
|
701
|
+
children: /* @__PURE__ */ jsx(CodeEditor, {
|
|
702
|
+
"data-testid": "files-source",
|
|
703
|
+
value: content,
|
|
704
|
+
path: relPath,
|
|
705
|
+
onChange: setSource,
|
|
706
|
+
onSelect: (range) => {
|
|
707
|
+
selection.current = range;
|
|
708
|
+
},
|
|
709
|
+
className: "h-full rounded border border-doom-border"
|
|
710
|
+
})
|
|
711
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
712
|
+
className: "flex shrink-0 items-center gap-2",
|
|
713
|
+
children: [
|
|
714
|
+
/* @__PURE__ */ jsx(Button, {
|
|
715
|
+
variant: "outline",
|
|
716
|
+
size: "xs",
|
|
717
|
+
"data-testid": "files-save",
|
|
718
|
+
loading: saving,
|
|
719
|
+
disabled: source === void 0 || saving,
|
|
720
|
+
onClick: () => void save(),
|
|
721
|
+
className: "",
|
|
722
|
+
children: "save to disk"
|
|
723
|
+
}),
|
|
724
|
+
source === void 0 ? null : /* @__PURE__ */ jsx(Button, {
|
|
725
|
+
variant: "ghost",
|
|
726
|
+
size: "xs",
|
|
727
|
+
"data-testid": "files-revert",
|
|
728
|
+
onClick: () => setSource(void 0),
|
|
729
|
+
className: "",
|
|
730
|
+
children: "discard edits"
|
|
731
|
+
}),
|
|
732
|
+
saveNote === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
733
|
+
"data-testid": "files-save-note",
|
|
734
|
+
className: saveNote === "saved" ? "text-sm text-doom-green" : "text-sm text-doom-red",
|
|
735
|
+
children: saveNote
|
|
736
|
+
})
|
|
737
|
+
]
|
|
738
|
+
})] })
|
|
739
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
740
|
+
"data-testid": "files-preview",
|
|
741
|
+
"data-mode": previewMode,
|
|
742
|
+
className: previewMode === "code" ? "flex h-full flex-col gap-2 p-4 text-base text-doom-text" : "flex flex-col gap-2 p-4 text-base text-doom-text",
|
|
743
|
+
onMouseUp: () => {
|
|
744
|
+
if (previewMode === "code") {
|
|
745
|
+
openSelectionDraft();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const text = globalThis.getSelection?.()?.toString() ?? "";
|
|
749
|
+
if (text.trim() !== "") openDraft({ snippet: text });
|
|
750
|
+
},
|
|
751
|
+
children: [previewMode === "unavailable" ? /* @__PURE__ */ jsx("p", {
|
|
752
|
+
"data-testid": "files-preview-unavailable",
|
|
753
|
+
className: "text-sm text-doom-dim",
|
|
754
|
+
children: working?.reason ?? "this file cannot be shown"
|
|
755
|
+
}) : null, previewMode === "media" || previewMode === "unavailable" ? /* @__PURE__ */ jsx(SessionMediaPreview, {
|
|
756
|
+
src: mediaSrc,
|
|
757
|
+
path: relPath,
|
|
758
|
+
"data-testid": "files-preview-media"
|
|
759
|
+
}) : previewMode === "markdown" ? /* @__PURE__ */ jsx(Markdown, {
|
|
760
|
+
text: content,
|
|
761
|
+
loadImage
|
|
762
|
+
}) : previewMode === "html" ? /* @__PURE__ */ jsx("iframe", {
|
|
763
|
+
"data-testid": "files-preview-html",
|
|
764
|
+
title: relPath,
|
|
765
|
+
sandbox: "",
|
|
766
|
+
srcDoc: content,
|
|
767
|
+
className: "h-[36rem] w-full rounded border border-doom-border bg-doom-deep"
|
|
768
|
+
}) : previewMode === "code" ? /* @__PURE__ */ jsx(CodeEditor, {
|
|
769
|
+
"data-testid": "files-preview-code",
|
|
770
|
+
value: content,
|
|
771
|
+
path: relPath,
|
|
772
|
+
readOnly: true,
|
|
773
|
+
onSelect: (range) => {
|
|
774
|
+
selection.current = range;
|
|
775
|
+
},
|
|
776
|
+
className: "min-h-[24rem] flex-1 rounded border border-doom-border"
|
|
777
|
+
}) : /* @__PURE__ */ jsx("pre", {
|
|
778
|
+
"data-testid": "files-preview-text",
|
|
779
|
+
className: "whitespace-pre-wrap break-words font-mono text-sm leading-normal text-doom-text",
|
|
780
|
+
children: content
|
|
781
|
+
})]
|
|
782
|
+
})
|
|
783
|
+
}),
|
|
784
|
+
draft === void 0 ? null : /* @__PURE__ */ jsx(ReviewCommentDraft, {
|
|
785
|
+
testIdPrefix: "files-comment",
|
|
786
|
+
snippet: draft.snippet,
|
|
787
|
+
...draft.side === void 0 ? {} : { side: draft.side },
|
|
788
|
+
...draft.startLine === void 0 ? {} : { startLine: draft.startLine },
|
|
789
|
+
...draft.endLine === void 0 ? {} : { endLine: draft.endLine },
|
|
790
|
+
onSubmit: submitDraft,
|
|
791
|
+
onCancel: () => setDraft(void 0)
|
|
792
|
+
}),
|
|
793
|
+
comments.length === 0 ? null : /* @__PURE__ */ jsxs("footer", {
|
|
794
|
+
"data-testid": "files-review",
|
|
795
|
+
className: "shrink-0 border-t border-doom-border px-3 py-2",
|
|
796
|
+
children: [
|
|
797
|
+
/* @__PURE__ */ jsxs("div", {
|
|
798
|
+
className: "flex items-center gap-2 pb-1",
|
|
799
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
800
|
+
className: "flex-1 text-base font-semibold text-doom-hi",
|
|
801
|
+
children: [
|
|
802
|
+
comments.length,
|
|
803
|
+
" ",
|
|
804
|
+
comments.length === 1 ? "comment" : "comments"
|
|
805
|
+
]
|
|
806
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
807
|
+
variant: "outline",
|
|
808
|
+
size: "xs",
|
|
809
|
+
"data-testid": "files-send-review",
|
|
810
|
+
onClick: sendReview,
|
|
811
|
+
className: "",
|
|
812
|
+
children: "send review"
|
|
813
|
+
})]
|
|
814
|
+
}),
|
|
815
|
+
reviewError === void 0 ? null : /* @__PURE__ */ jsxs("p", {
|
|
816
|
+
role: "alert",
|
|
817
|
+
"data-testid": "files-review-error",
|
|
818
|
+
className: "pb-1 text-sm text-doom-red",
|
|
819
|
+
children: ["review not sent: ", reviewError]
|
|
820
|
+
}),
|
|
821
|
+
comments.map((comment) => /* @__PURE__ */ jsxs("div", {
|
|
822
|
+
"data-testid": "files-review-comment",
|
|
823
|
+
className: "flex items-start gap-2 py-0.5",
|
|
824
|
+
children: [
|
|
825
|
+
/* @__PURE__ */ jsx("span", {
|
|
826
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
827
|
+
children: reviewCommentAnchor(comment)
|
|
828
|
+
}),
|
|
829
|
+
/* @__PURE__ */ jsx("span", {
|
|
830
|
+
className: "min-w-0 flex-1 truncate text-base text-doom-text",
|
|
831
|
+
children: comment.body
|
|
832
|
+
}),
|
|
833
|
+
/* @__PURE__ */ jsx(Button, {
|
|
834
|
+
variant: "ghost",
|
|
835
|
+
size: "xs",
|
|
836
|
+
"data-testid": "files-review-remove",
|
|
837
|
+
onClick: () => {
|
|
838
|
+
if (sessionId !== null) removeComment(sessionId, comment.id);
|
|
839
|
+
},
|
|
840
|
+
className: "shrink-0 ",
|
|
841
|
+
children: "remove"
|
|
842
|
+
})
|
|
843
|
+
]
|
|
844
|
+
}, comment.id))
|
|
845
|
+
]
|
|
846
|
+
})
|
|
847
|
+
]
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
/** The transient tab one file opens in; the same file reopens rather than duplicating. */
|
|
851
|
+
function fileTab(filePath, relPath) {
|
|
852
|
+
return {
|
|
853
|
+
id: fileTabId(filePath),
|
|
854
|
+
label: relPath.split("/").at(-1) ?? relPath,
|
|
855
|
+
panel: (props) => /* @__PURE__ */ jsx(FilePanel, {
|
|
856
|
+
...props,
|
|
857
|
+
filePath,
|
|
858
|
+
relPath
|
|
859
|
+
})
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/extensions/workspaces/sessions/(frontend)/file-links/_components/FilePreviewPanel.tsx
|
|
864
|
+
function FilePreviewPanel({ filePath, sessionId, closeTransientTab }) {
|
|
865
|
+
const [loaded, setLoaded] = useState(void 0);
|
|
866
|
+
useEffect(() => {
|
|
867
|
+
if (sessionId === null) return;
|
|
868
|
+
let cancelled = false;
|
|
869
|
+
fetchFilePreview(sessionId, filePath).then((result) => {
|
|
870
|
+
if (cancelled) return;
|
|
871
|
+
setLoaded(result.ok ? {
|
|
872
|
+
sessionId,
|
|
873
|
+
path: filePath,
|
|
874
|
+
preview: result.preview
|
|
875
|
+
} : {
|
|
876
|
+
sessionId,
|
|
877
|
+
path: filePath,
|
|
878
|
+
error: result.error
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
return () => {
|
|
882
|
+
cancelled = true;
|
|
883
|
+
};
|
|
884
|
+
}, [sessionId, filePath]);
|
|
885
|
+
const current = loaded?.path === filePath && loaded.sessionId === sessionId ? loaded : void 0;
|
|
886
|
+
const preview = current?.preview;
|
|
887
|
+
const error = current?.error;
|
|
888
|
+
const relPath = preview?.relPath ?? filePath;
|
|
889
|
+
const working = preview?.working;
|
|
890
|
+
const content = working?.content ?? "";
|
|
891
|
+
const previewMode = previewModeOf(relPath, working?.unavailable === true);
|
|
892
|
+
const loadImage = useCallback((path) => loadMarkdownImage(sessionId, relPath, path), [sessionId, relPath]);
|
|
893
|
+
const mediaSrc = sessionId === null ? "" : sessionFileUrl(sessionId, relPath);
|
|
894
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
895
|
+
"data-testid": "files-preview-panel",
|
|
896
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
897
|
+
children: [
|
|
898
|
+
/* @__PURE__ */ jsxs("header", {
|
|
899
|
+
className: "flex shrink-0 items-center gap-2 border-b border-doom-border px-4 py-2",
|
|
900
|
+
children: [
|
|
901
|
+
/* @__PURE__ */ jsx(Breadcrumb, {
|
|
902
|
+
path: relPath,
|
|
903
|
+
"data-testid": "files-preview-breadcrumb",
|
|
904
|
+
className: "min-w-0 flex-1"
|
|
905
|
+
}),
|
|
906
|
+
/* @__PURE__ */ jsx("span", {
|
|
907
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
908
|
+
children: "unchanged"
|
|
909
|
+
}),
|
|
910
|
+
/* @__PURE__ */ jsx(Button, {
|
|
911
|
+
variant: "ghost",
|
|
912
|
+
size: "xs",
|
|
913
|
+
"data-testid": "files-preview-close",
|
|
914
|
+
onClick: () => closeTransientTab(filePreviewTabId(filePath)),
|
|
915
|
+
className: "shrink-0 ",
|
|
916
|
+
children: "close"
|
|
917
|
+
})
|
|
918
|
+
]
|
|
919
|
+
}),
|
|
920
|
+
error === void 0 ? null : /* @__PURE__ */ jsx("p", {
|
|
921
|
+
"data-testid": "files-preview-error",
|
|
922
|
+
className: "px-4 py-3 text-sm text-doom-red",
|
|
923
|
+
children: error
|
|
924
|
+
}),
|
|
925
|
+
preview === void 0 && error === void 0 ? /* @__PURE__ */ jsx("p", {
|
|
926
|
+
"data-testid": "files-preview-loading",
|
|
927
|
+
className: "px-4 py-3 text-sm text-doom-dim",
|
|
928
|
+
children: "reading this file…"
|
|
929
|
+
}) : null,
|
|
930
|
+
preview === void 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
931
|
+
className: "min-h-0 flex-1 overflow-y-auto",
|
|
932
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
933
|
+
"data-testid": "files-preview",
|
|
934
|
+
"data-mode": previewMode,
|
|
935
|
+
className: "flex h-full flex-col gap-2 p-4 text-base text-doom-text",
|
|
936
|
+
children: [previewMode === "unavailable" ? /* @__PURE__ */ jsx("p", {
|
|
937
|
+
"data-testid": "files-preview-unavailable",
|
|
938
|
+
className: "text-sm text-doom-dim",
|
|
939
|
+
children: working?.reason ?? "this file cannot be shown"
|
|
940
|
+
}) : null, previewMode === "media" || previewMode === "unavailable" ? /* @__PURE__ */ jsx(SessionMediaPreview, {
|
|
941
|
+
src: mediaSrc,
|
|
942
|
+
path: relPath,
|
|
943
|
+
"data-testid": "files-preview-media"
|
|
944
|
+
}) : previewMode === "markdown" ? /* @__PURE__ */ jsx(Markdown, {
|
|
945
|
+
text: content,
|
|
946
|
+
loadImage
|
|
947
|
+
}) : previewMode === "html" ? /* @__PURE__ */ jsx("iframe", {
|
|
948
|
+
"data-testid": "files-preview-html",
|
|
949
|
+
title: relPath,
|
|
950
|
+
sandbox: "",
|
|
951
|
+
srcDoc: content,
|
|
952
|
+
className: "h-[36rem] w-full rounded border border-doom-border bg-doom-deep"
|
|
953
|
+
}) : previewMode === "code" ? /* @__PURE__ */ jsx(CodeEditor, {
|
|
954
|
+
"data-testid": "files-preview-code",
|
|
955
|
+
value: content,
|
|
956
|
+
path: relPath,
|
|
957
|
+
readOnly: true,
|
|
958
|
+
className: "min-h-[24rem] flex-1 rounded border border-doom-border"
|
|
959
|
+
}) : /* @__PURE__ */ jsx("pre", {
|
|
960
|
+
"data-testid": "files-preview-text",
|
|
961
|
+
className: "whitespace-pre-wrap break-words font-mono text-sm leading-normal text-doom-text",
|
|
962
|
+
children: content
|
|
963
|
+
})]
|
|
964
|
+
})
|
|
965
|
+
})
|
|
966
|
+
]
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* The id of the read-only tab, kept apart from the changed-file tab's own id:
|
|
971
|
+
* the same file can be read now and changed later, and the two tabs are not
|
|
972
|
+
* the same view of it.
|
|
973
|
+
*/
|
|
974
|
+
function filePreviewTabId(filePath) {
|
|
975
|
+
return `${fileTabId(filePath)}-preview`;
|
|
976
|
+
}
|
|
977
|
+
/** The transient tab an unchanged file opens in; the same file reopens rather than duplicating. */
|
|
978
|
+
function filePreviewTab(filePath) {
|
|
979
|
+
return {
|
|
980
|
+
id: filePreviewTabId(filePath),
|
|
981
|
+
label: filePath.split("/").at(-1) ?? filePath,
|
|
982
|
+
panel: (props) => /* @__PURE__ */ jsx(FilePreviewPanel, {
|
|
983
|
+
...props,
|
|
984
|
+
filePath
|
|
985
|
+
})
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/extensions/workspaces/sessions/(frontend)/file-links/_lib/fileLinks.ts
|
|
990
|
+
/**
|
|
991
|
+
* The files a message names, as links into the same tab the activity dock
|
|
992
|
+
* opens.
|
|
993
|
+
*
|
|
994
|
+
* Only files this session recorded a change to are claimed. A message quotes a
|
|
995
|
+
* great deal that is shaped like nothing in particular, class names and globs
|
|
996
|
+
* and flags among it, and guessing at what is a path produces links that open
|
|
997
|
+
* an error. The recorded set is the one thing the page knows for certain, and
|
|
998
|
+
* it is also the set a reader is most likely to want open.
|
|
999
|
+
*/
|
|
1000
|
+
/** A reference the way a message writes one: 'src/app.ts:42' or 'src/app.ts:42:9'. */
|
|
1001
|
+
const LINE_SUFFIX = /:\d+(?::\d+)?$/;
|
|
1002
|
+
function itemsFor(sessionId) {
|
|
1003
|
+
return files.select(files.store.state, sessionId).items;
|
|
1004
|
+
}
|
|
1005
|
+
function match(sessionId, path) {
|
|
1006
|
+
const candidate = path.trim().replace(LINE_SUFFIX, "");
|
|
1007
|
+
if (candidate === "") return void 0;
|
|
1008
|
+
return itemsFor(sessionId).find((item) => item.relPath === candidate || item.path === candidate);
|
|
1009
|
+
}
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/extensions/workspaces/sessions/(frontend)/file-links/files.web.ts
|
|
1012
|
+
var files_web_default$1 = defineFileLinks({
|
|
1013
|
+
subscribe(listener) {
|
|
1014
|
+
const subscription = files.store.subscribe(listener);
|
|
1015
|
+
return () => subscription.unsubscribe();
|
|
1016
|
+
},
|
|
1017
|
+
fingerprint: (sessionId) => itemsFor(sessionId).map((item) => item.relPath).join("\n"),
|
|
1018
|
+
resolve: (sessionId, path) => {
|
|
1019
|
+
const item = match(sessionId, path);
|
|
1020
|
+
return item === void 0 ? void 0 : fileTab(item.path, item.relPath);
|
|
1021
|
+
},
|
|
1022
|
+
openPath: (sessionId, path) => {
|
|
1023
|
+
const item = match(sessionId, path);
|
|
1024
|
+
if (item !== void 0) return fileTab(item.path, item.relPath);
|
|
1025
|
+
const candidate = path.trim();
|
|
1026
|
+
return candidate === "" ? void 0 : filePreviewTab(candidate);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
//#endregion
|
|
1030
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/_components/FilesBrowser.tsx
|
|
1031
|
+
function FileMetadata({ item, label, recent = false }) {
|
|
1032
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
1033
|
+
className: "flex min-w-0 w-full items-center gap-1.5",
|
|
1034
|
+
children: [
|
|
1035
|
+
/* @__PURE__ */ jsx("span", {
|
|
1036
|
+
className: "min-w-0 flex-1 truncate text-left text-base font-semibold text-doom-hi",
|
|
1037
|
+
children: label ?? item.relPath
|
|
1038
|
+
}),
|
|
1039
|
+
recent ? /* @__PURE__ */ jsx("span", {
|
|
1040
|
+
"data-recent": "true",
|
|
1041
|
+
title: "most recent change",
|
|
1042
|
+
className: "shrink-0 text-2xs text-doom-blue",
|
|
1043
|
+
children: "●"
|
|
1044
|
+
}) : null,
|
|
1045
|
+
item.count > 1 ? /* @__PURE__ */ jsxs("span", {
|
|
1046
|
+
className: "shrink-0 text-2xs text-doom-faint",
|
|
1047
|
+
children: [item.count, "×"]
|
|
1048
|
+
}) : null,
|
|
1049
|
+
/* @__PURE__ */ jsx("span", {
|
|
1050
|
+
className: `shrink-0 text-sm ${item.diffable ? "text-doom-dim" : "text-doom-yellow"}`,
|
|
1051
|
+
children: TOOL_LABEL[item.tool] ?? item.tool
|
|
1052
|
+
})
|
|
1053
|
+
]
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
function FileActivityRow({ item, onOpen }) {
|
|
1057
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
1058
|
+
variant: "ghost",
|
|
1059
|
+
size: "card",
|
|
1060
|
+
"data-testid": `activity-file-${item.relPath}`,
|
|
1061
|
+
"data-file-diffable": item.diffable,
|
|
1062
|
+
title: item.diffable ? item.path : `${item.path} (changed by a command, so no diff was captured)`,
|
|
1063
|
+
onClick: onOpen,
|
|
1064
|
+
className: "min-w-0 gap-0.5 rounded-md px-1 py-1 hover:bg-doom-panel",
|
|
1065
|
+
children: /* @__PURE__ */ jsx(FileMetadata, { item })
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* The directory a run of rows shares.
|
|
1070
|
+
*
|
|
1071
|
+
* Deliberately not an option: the cursor counts files, and a header is a label
|
|
1072
|
+
* rather than something to open. Keeping it out of the listbox is what lets
|
|
1073
|
+
* arrow keys and Enter stay exactly what they were before the tree existed.
|
|
1074
|
+
*/
|
|
1075
|
+
function FileGroupHeader({ prefix }) {
|
|
1076
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1077
|
+
"data-testid": `files-browser-group-${prefix}`,
|
|
1078
|
+
className: "truncate px-4 pt-2 pb-1 text-sm font-semibold text-doom-dim",
|
|
1079
|
+
children: prefix
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
function BrowserFileRow({ item, label, indented = false, recent = false, selected, onSelect, onOpen }) {
|
|
1083
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1084
|
+
role: "option",
|
|
1085
|
+
"aria-selected": selected,
|
|
1086
|
+
tabIndex: -1,
|
|
1087
|
+
"data-testid": `files-browser-file-${item.relPath}`,
|
|
1088
|
+
"data-file-diffable": item.diffable,
|
|
1089
|
+
"data-selected": selected,
|
|
1090
|
+
title: item.diffable ? item.path : `${item.path} (changed by a command, so no diff was captured)`,
|
|
1091
|
+
onMouseEnter: onSelect,
|
|
1092
|
+
onClick: onOpen,
|
|
1093
|
+
className: `flex min-w-0 cursor-pointer border-l-2 py-2 ${indented ? "pr-4 pl-8" : "px-4"} ${selected ? "border-doom-blue bg-doom-panel" : "border-transparent hover:bg-doom-panel/60"}`,
|
|
1094
|
+
children: /* @__PURE__ */ jsx(FileMetadata, {
|
|
1095
|
+
item,
|
|
1096
|
+
label,
|
|
1097
|
+
recent
|
|
1098
|
+
})
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
/** The complete session file picker, ordered exactly as the file timeline supplies it. */
|
|
1102
|
+
function FilesBrowser({ items, onClose, onOpen }) {
|
|
1103
|
+
const [query, setQuery] = useState("");
|
|
1104
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
1105
|
+
const inputRef = useRef(null);
|
|
1106
|
+
const listRef = useRef(null);
|
|
1107
|
+
const shown = filterFileItems(items, query);
|
|
1108
|
+
const grouped = query.trim() === "" ? groupByDirectory(shown, (item) => item.relPath) : void 0;
|
|
1109
|
+
const ordered = grouped === void 0 ? shown : grouped.flatMap((group) => group.items);
|
|
1110
|
+
const positions = new Map(ordered.map((item, index) => [item.path, index]));
|
|
1111
|
+
const selected = ordered[selectedIndex];
|
|
1112
|
+
const recentPath = items[0]?.path;
|
|
1113
|
+
const [lastShownLength, setLastShownLength] = useState(shown.length);
|
|
1114
|
+
if (lastShownLength !== shown.length) {
|
|
1115
|
+
setLastShownLength(shown.length);
|
|
1116
|
+
setSelectedIndex((current) => Math.min(current, Math.max(0, shown.length - 1)));
|
|
1117
|
+
}
|
|
1118
|
+
useEffect(() => {
|
|
1119
|
+
listRef.current?.querySelector("[aria-selected=\"true\"]")?.scrollIntoView({ block: "nearest" });
|
|
1120
|
+
}, [selectedIndex]);
|
|
1121
|
+
const move = (delta) => {
|
|
1122
|
+
if (ordered.length === 0) return;
|
|
1123
|
+
setSelectedIndex((current) => (current + delta + ordered.length) % ordered.length);
|
|
1124
|
+
};
|
|
1125
|
+
const onKeyDown = (event) => {
|
|
1126
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
1127
|
+
event.preventDefault();
|
|
1128
|
+
move(event.key === "ArrowDown" ? 1 : -1);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (event.key === "Escape") {
|
|
1132
|
+
event.preventDefault();
|
|
1133
|
+
onClose();
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (event.key === "Enter" && selected !== void 0 && (!(event.target instanceof Element) || event.target.closest("button") === null)) {
|
|
1137
|
+
event.preventDefault();
|
|
1138
|
+
onOpen(selected);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
return /* @__PURE__ */ jsxs("aside", {
|
|
1142
|
+
"data-testid": "files-browser",
|
|
1143
|
+
"aria-label": "changed files",
|
|
1144
|
+
tabIndex: -1,
|
|
1145
|
+
onKeyDown,
|
|
1146
|
+
className: "fixed inset-y-0 right-0 z-50 flex w-[min(440px,calc(100vw-24px))] flex-col overflow-hidden border-l border-doom-border bg-doom-rail outline-none",
|
|
1147
|
+
children: [
|
|
1148
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1149
|
+
className: "flex h-11 shrink-0 items-center gap-2.5 border-b border-doom-border px-4",
|
|
1150
|
+
children: [
|
|
1151
|
+
/* @__PURE__ */ jsx("span", {
|
|
1152
|
+
className: "text-lg font-semibold text-doom-hi",
|
|
1153
|
+
children: "# files"
|
|
1154
|
+
}),
|
|
1155
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1156
|
+
"data-testid": "files-browser-total",
|
|
1157
|
+
className: "text-sm text-doom-dim",
|
|
1158
|
+
children: [items.length, " changed"]
|
|
1159
|
+
}),
|
|
1160
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
|
|
1161
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1162
|
+
variant: "ghost",
|
|
1163
|
+
size: "icon",
|
|
1164
|
+
"data-testid": "files-browser-close",
|
|
1165
|
+
"aria-label": "close changed files",
|
|
1166
|
+
title: "close changed files",
|
|
1167
|
+
onClick: onClose,
|
|
1168
|
+
children: /* @__PURE__ */ jsx(CloseIcon, { className: "h-3 w-3" })
|
|
1169
|
+
})
|
|
1170
|
+
]
|
|
1171
|
+
}),
|
|
1172
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1173
|
+
className: "flex items-center gap-2 px-4 pt-2.5 pb-1.5",
|
|
1174
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
1175
|
+
ref: inputRef,
|
|
1176
|
+
"data-testid": "files-browser-search",
|
|
1177
|
+
"aria-label": "search changed file paths",
|
|
1178
|
+
value: query,
|
|
1179
|
+
placeholder: "search paths…",
|
|
1180
|
+
autoFocus: true,
|
|
1181
|
+
onChange: (event) => {
|
|
1182
|
+
setQuery(event.target.value);
|
|
1183
|
+
setSelectedIndex(0);
|
|
1184
|
+
}
|
|
1185
|
+
}), query.length > 0 ? /* @__PURE__ */ jsx(Button, {
|
|
1186
|
+
variant: "ghost",
|
|
1187
|
+
size: "xs",
|
|
1188
|
+
"data-testid": "files-browser-clear",
|
|
1189
|
+
"aria-label": "clear path search",
|
|
1190
|
+
onClick: () => {
|
|
1191
|
+
setQuery("");
|
|
1192
|
+
setSelectedIndex(0);
|
|
1193
|
+
inputRef.current?.focus();
|
|
1194
|
+
},
|
|
1195
|
+
children: "clear"
|
|
1196
|
+
}) : null]
|
|
1197
|
+
}),
|
|
1198
|
+
/* @__PURE__ */ jsxs("p", {
|
|
1199
|
+
"data-testid": "files-browser-matches",
|
|
1200
|
+
className: "shrink-0 px-4 pb-2 text-sm text-doom-dim",
|
|
1201
|
+
children: [
|
|
1202
|
+
shown.length,
|
|
1203
|
+
" matches · ",
|
|
1204
|
+
grouped === void 0 ? "newest change first" : "by path"
|
|
1205
|
+
]
|
|
1206
|
+
}),
|
|
1207
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1208
|
+
ref: listRef,
|
|
1209
|
+
role: "listbox",
|
|
1210
|
+
"aria-label": "changed files",
|
|
1211
|
+
className: "flex min-h-0 flex-1 flex-col overflow-y-auto pb-2",
|
|
1212
|
+
children: [shown.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
1213
|
+
className: "px-4 py-3 text-sm text-doom-dim",
|
|
1214
|
+
children: "nothing matches the path"
|
|
1215
|
+
}) : null, grouped === void 0 ? shown.map((item) => /* @__PURE__ */ jsx(BrowserFileRow, {
|
|
1216
|
+
item,
|
|
1217
|
+
label: item.relPath,
|
|
1218
|
+
recent: item.path === recentPath,
|
|
1219
|
+
selected: positions.get(item.path) === selectedIndex,
|
|
1220
|
+
onSelect: () => setSelectedIndex(positions.get(item.path) ?? 0),
|
|
1221
|
+
onOpen: () => onOpen(item)
|
|
1222
|
+
}, item.path)) : grouped.map((group) => /* @__PURE__ */ jsxs(Fragment, { children: [group.prefix === "" ? null : /* @__PURE__ */ jsx(FileGroupHeader, { prefix: group.prefix }), group.items.map((item) => /* @__PURE__ */ jsx(BrowserFileRow, {
|
|
1223
|
+
item,
|
|
1224
|
+
label: groupRowLabel(group.prefix, item.relPath),
|
|
1225
|
+
indented: true,
|
|
1226
|
+
recent: item.path === recentPath,
|
|
1227
|
+
selected: positions.get(item.path) === selectedIndex,
|
|
1228
|
+
onSelect: () => setSelectedIndex(positions.get(item.path) ?? 0),
|
|
1229
|
+
onOpen: () => onOpen(item)
|
|
1230
|
+
}, item.path))] }, group.prefix))]
|
|
1231
|
+
}),
|
|
1232
|
+
/* @__PURE__ */ jsx("div", {
|
|
1233
|
+
className: "flex h-8 shrink-0 items-center border-t border-doom-border-soft bg-doom-deep px-4",
|
|
1234
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1235
|
+
className: "text-sm text-doom-dim",
|
|
1236
|
+
children: "↑↓ choose · enter open · esc close"
|
|
1237
|
+
})
|
|
1238
|
+
})
|
|
1239
|
+
]
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/_components/FilesActivitySection.tsx
|
|
1244
|
+
/**
|
|
1245
|
+
* The files group's body in the activity dock: what this session has changed.
|
|
1246
|
+
*
|
|
1247
|
+
* Every recorded file is listed, not only the ones the agent named. A file
|
|
1248
|
+
* found by comparing the tree around a bash call is marked, because it is
|
|
1249
|
+
* openable and editable but has no baseline to diff against, and a row that
|
|
1250
|
+
* hid that distinction would promise a diff the tab cannot show.
|
|
1251
|
+
*/
|
|
1252
|
+
/** The most recent changes first, which is the order the timeline already folds to. */
|
|
1253
|
+
const VISIBLE_ROWS = 5;
|
|
1254
|
+
function FilesActivitySection({ sessionId, openTransientTab }) {
|
|
1255
|
+
const { items } = useStore(files.store, (state) => files.select(state, sessionId));
|
|
1256
|
+
const [browserOpen, setBrowserOpen] = useState(false);
|
|
1257
|
+
const openFile = (item) => {
|
|
1258
|
+
if (sessionId === null) return;
|
|
1259
|
+
openTransientTab(fileTab(item.path, item.relPath));
|
|
1260
|
+
setBrowserOpen(false);
|
|
1261
|
+
};
|
|
1262
|
+
if (items.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
1263
|
+
"data-testid": "activity-summary-files",
|
|
1264
|
+
className: "px-1 text-sm text-doom-dim",
|
|
1265
|
+
children: "nothing changed yet"
|
|
1266
|
+
});
|
|
1267
|
+
const visible = items.slice(0, VISIBLE_ROWS);
|
|
1268
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1269
|
+
"data-testid": "activity-file-edits",
|
|
1270
|
+
className: "flex flex-col gap-0.5",
|
|
1271
|
+
children: [
|
|
1272
|
+
visible.map((item) => /* @__PURE__ */ jsx(FileActivityRow, {
|
|
1273
|
+
item,
|
|
1274
|
+
onOpen: () => openFile(item)
|
|
1275
|
+
}, item.path)),
|
|
1276
|
+
items.length > visible.length ? /* @__PURE__ */ jsxs(Button, {
|
|
1277
|
+
variant: "link",
|
|
1278
|
+
size: "xs",
|
|
1279
|
+
"data-testid": "activity-files-show-all",
|
|
1280
|
+
"aria-label": `show all ${items.length} changed files`,
|
|
1281
|
+
className: "self-start px-1 pt-0.5 text-doom-faint",
|
|
1282
|
+
onClick: () => setBrowserOpen(true),
|
|
1283
|
+
children: [
|
|
1284
|
+
"show all ",
|
|
1285
|
+
items.length,
|
|
1286
|
+
" files"
|
|
1287
|
+
]
|
|
1288
|
+
}) : null,
|
|
1289
|
+
browserOpen ? /* @__PURE__ */ jsx(FilesBrowser, {
|
|
1290
|
+
items,
|
|
1291
|
+
onClose: () => setBrowserOpen(false),
|
|
1292
|
+
onOpen: openFile
|
|
1293
|
+
}) : null
|
|
1294
|
+
]
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/Files.activity.files.web.tsx
|
|
1299
|
+
var Files_activity_files_web_default = defineFill({ component: FilesActivitySection });
|
|
1300
|
+
var files_web_default = defineLeaderBinding({
|
|
1301
|
+
path: [{
|
|
1302
|
+
key: "e",
|
|
1303
|
+
label: "extension",
|
|
1304
|
+
detail: "tools, skills and config"
|
|
1305
|
+
}, {
|
|
1306
|
+
key: "f",
|
|
1307
|
+
label: "files",
|
|
1308
|
+
detail: "files this session changed"
|
|
1309
|
+
}],
|
|
1310
|
+
command: "file-edits"
|
|
1311
|
+
});
|
|
1312
|
+
//#endregion
|
|
1313
|
+
//#region generated/web.ts
|
|
1314
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
1315
|
+
...identity,
|
|
1316
|
+
...value
|
|
1317
|
+
};
|
|
1318
|
+
const webPlugin = defineWebPlugin({
|
|
1319
|
+
id: "files",
|
|
1320
|
+
session: {
|
|
1321
|
+
get activityGroups() {
|
|
1322
|
+
return [via({ name: "files" }, files_web_default$2)];
|
|
1323
|
+
},
|
|
1324
|
+
get channels() {
|
|
1325
|
+
return [via({ channel: "file_edits" }, file_edits_web_default)];
|
|
1326
|
+
},
|
|
1327
|
+
get fileLinks() {
|
|
1328
|
+
return files_web_default$1;
|
|
1329
|
+
},
|
|
1330
|
+
get fills() {
|
|
1331
|
+
return [via({
|
|
1332
|
+
slot: "activity.files",
|
|
1333
|
+
id: "files"
|
|
1334
|
+
}, Files_activity_files_web_default)];
|
|
1335
|
+
},
|
|
1336
|
+
get leaderBindings() {
|
|
1337
|
+
return [via({ id: "files.files" }, files_web_default)];
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
//#endregion
|
|
1342
|
+
export { webPlugin };
|
|
1343
|
+
|
|
1344
|
+
//# sourceMappingURL=web.mjs.map
|