@agimon-ai/po-artifact 0.0.0-alpha.1
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 +18 -0
- package/dist/apiContracts.cjs +112 -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 +107 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/extensions/native.mjs +700 -0
- package/dist/extensions/native.mjs.map +1 -0
- package/dist/extensions/server.cjs +561 -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 +532 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +507 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index.cjs +0 -0
- package/dist/index.d.mts +22 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +152 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineNativePlugin, defineSessionStore, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
|
|
2
|
+
import { Text } from "react-native-unistyles/components/native/Text";
|
|
3
|
+
import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
|
|
4
|
+
import { Button, CodeBlock, FolderIcon, Markdown, fontOf } from "@agimon-ai/po-native-components";
|
|
5
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
6
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
7
|
+
import { View } from "react-native-unistyles/components/native/View";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
const api = createApiClient({
|
|
10
|
+
list: {
|
|
11
|
+
method: "GET",
|
|
12
|
+
path: "/",
|
|
13
|
+
response: void 0
|
|
14
|
+
},
|
|
15
|
+
preview: {
|
|
16
|
+
method: "GET",
|
|
17
|
+
path: "/preview",
|
|
18
|
+
query: ["path"],
|
|
19
|
+
response: void 0
|
|
20
|
+
},
|
|
21
|
+
content: {
|
|
22
|
+
method: "GET",
|
|
23
|
+
path: "/content",
|
|
24
|
+
query: ["path"],
|
|
25
|
+
raw: true
|
|
26
|
+
}
|
|
27
|
+
}, {
|
|
28
|
+
scopes: ["session"],
|
|
29
|
+
basePath: "artifacts",
|
|
30
|
+
transport: nativeApiTransport,
|
|
31
|
+
sessionAddress: sessionApiAddress
|
|
32
|
+
});
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/artifactView.ts
|
|
35
|
+
function sortedArtifacts(items) {
|
|
36
|
+
return [...items].sort((left, right) => right.modifiedAt - left.modifiedAt || (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
37
|
+
}
|
|
38
|
+
function artifactTabId(sessionId, path) {
|
|
39
|
+
return `artifact:${encodeURIComponent(sessionId)}:${encodeURIComponent(path)}`;
|
|
40
|
+
}
|
|
41
|
+
/** Resolves document assets within the artifact folder, never into another filesystem root. */
|
|
42
|
+
function artifactAssetPath(documentPath, imagePath) {
|
|
43
|
+
if (!imagePath || imagePath.startsWith("/") || imagePath.includes("\\") || /^[a-z][a-z0-9+.-]*:/iu.test(imagePath)) throw new Error("Artifact images must use a relative path.");
|
|
44
|
+
const segments = documentPath.split("/").slice(0, -1);
|
|
45
|
+
for (const segment of imagePath.split("/")) {
|
|
46
|
+
if (segment === "" || segment === ".") continue;
|
|
47
|
+
if (segment === "..") {
|
|
48
|
+
if (segments.length === 0) throw new Error("The image is outside this artifact folder.");
|
|
49
|
+
segments.pop();
|
|
50
|
+
} else segments.push(segment);
|
|
51
|
+
}
|
|
52
|
+
if (segments.length === 0) throw new Error("The image path is empty.");
|
|
53
|
+
return segments.join("/");
|
|
54
|
+
}
|
|
55
|
+
function artifactPreviewMode(path) {
|
|
56
|
+
if (/\.(?:png|jpe?g|gif|webp|avif|bmp|svg|ico|mp3|wav|m4a|ogg|mp4|mov|webm|pdf)$/iu.test(path)) return "media";
|
|
57
|
+
if (/\.(?:md|markdown)$/iu.test(path)) return "markdown";
|
|
58
|
+
if (/\.html?$/iu.test(path)) return "html";
|
|
59
|
+
if (/\.(?:[cm]?[jt]sx?|json|ya?ml|css|scss|py|sh|bash|go|rs|rb|java|c|cpp|h|sql|xml|toml)$/iu.test(path)) return "code";
|
|
60
|
+
return "text";
|
|
61
|
+
}
|
|
62
|
+
/** A malformed channel frame must not replace the current session's known inventory. */
|
|
63
|
+
function parseArtifactSnapshot(input) {
|
|
64
|
+
if (typeof input !== "object" || input === null || !("items" in input) || !Array.isArray(input.items)) return null;
|
|
65
|
+
if ("error" in input && input.error !== void 0 && typeof input.error !== "string") return null;
|
|
66
|
+
const items = [];
|
|
67
|
+
for (const item of input.items) {
|
|
68
|
+
if (typeof item !== "object" || item === null || typeof item.path !== "string" || !item.path || item.path.startsWith("/") || item.path.includes("\\") || item.path.split("/").some((part) => part === ".." || part === ".") || typeof item.name !== "string" || !item.name || typeof item.size !== "number" || !Number.isFinite(item.size) || item.size < 0 || typeof item.modifiedAt !== "number" || !Number.isFinite(item.modifiedAt) || item.modifiedAt < 0) return null;
|
|
69
|
+
items.push({
|
|
70
|
+
path: item.path,
|
|
71
|
+
name: item.name,
|
|
72
|
+
size: item.size,
|
|
73
|
+
modifiedAt: item.modifiedAt
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
items: sortedArtifacts(items),
|
|
78
|
+
..."error" in input && typeof input.error === "string" ? { error: input.error } : {}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/artifactsApi.native.ts
|
|
83
|
+
const MAX_MEDIA_BYTES = 5242880;
|
|
84
|
+
async function fetchArtifacts(sessionId, signal) {
|
|
85
|
+
const result = await api.session(sessionId).list({ signal });
|
|
86
|
+
if (!result.ok) throw new Error(result.error || "Artifacts could not be loaded.");
|
|
87
|
+
const snapshot = parseArtifactSnapshot(result.data);
|
|
88
|
+
if (!snapshot) throw new Error("The session returned an invalid artifact list.");
|
|
89
|
+
return snapshot;
|
|
90
|
+
}
|
|
91
|
+
async function fetchArtifactPreview(sessionId, path, signal) {
|
|
92
|
+
const result = await api.session(sessionId).preview({
|
|
93
|
+
query: { path },
|
|
94
|
+
signal
|
|
95
|
+
});
|
|
96
|
+
if (!result.ok) throw new Error(result.error || "This artifact could not be read.");
|
|
97
|
+
const preview = result.data;
|
|
98
|
+
if (!preview || typeof preview.path !== "string" || typeof preview.content !== "string" || typeof preview.mimeType !== "string") throw new Error("The session returned an invalid artifact preview.");
|
|
99
|
+
return preview;
|
|
100
|
+
}
|
|
101
|
+
/** No browser btoa or Node Buffer is available in an independently loaded native plugin. */
|
|
102
|
+
function base64(bytes) {
|
|
103
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
104
|
+
const parts = [];
|
|
105
|
+
let chunk = "";
|
|
106
|
+
for (let offset = 0; offset < bytes.length; offset += 3) {
|
|
107
|
+
const first = bytes[offset];
|
|
108
|
+
const second = bytes[offset + 1];
|
|
109
|
+
const third = bytes[offset + 2];
|
|
110
|
+
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]);
|
|
111
|
+
if (chunk.length >= 8192) {
|
|
112
|
+
parts.push(chunk);
|
|
113
|
+
chunk = "";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
parts.push(chunk);
|
|
117
|
+
return parts.join("");
|
|
118
|
+
}
|
|
119
|
+
async function fetchArtifactMedia(sessionId, path, signal) {
|
|
120
|
+
const result = await api.session(sessionId).content({
|
|
121
|
+
query: { path },
|
|
122
|
+
signal
|
|
123
|
+
});
|
|
124
|
+
if (!result.ok) throw new Error(result.error || "This media could not be loaded.");
|
|
125
|
+
const declaredLength = result.response.headers.get("content-length");
|
|
126
|
+
const length = declaredLength !== null && /^\d+$/u.test(declaredLength) ? Number(declaredLength) : NaN;
|
|
127
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > MAX_MEDIA_BYTES) throw new Error("This media is too large to preview.");
|
|
128
|
+
const bytes = new Uint8Array(await result.response.arrayBuffer());
|
|
129
|
+
if (signal.aborted) throw new Error("Media loading cancelled.");
|
|
130
|
+
if (bytes.byteLength > MAX_MEDIA_BYTES) throw new Error("This media is too large to preview.");
|
|
131
|
+
const mimeType = result.response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
132
|
+
if (!/^(?:image\/[a-z0-9.+-]+|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+|application\/pdf)$/u.test(mimeType)) throw new Error("This media type cannot be previewed.");
|
|
133
|
+
return {
|
|
134
|
+
data: base64(bytes),
|
|
135
|
+
mimeType
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** The native host owns saving, with bounded version-checked reads supplied by this package. */
|
|
139
|
+
async function prepareArtifactSave(sessionId, path, signal) {
|
|
140
|
+
const load = async (headers, signal) => {
|
|
141
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
142
|
+
const result = await api.session(sessionId).content({
|
|
143
|
+
query: { path },
|
|
144
|
+
headers,
|
|
145
|
+
signal
|
|
146
|
+
});
|
|
147
|
+
if (!result.ok && result.status !== 416) throw new Error(result.error || "This artifact could not be read.");
|
|
148
|
+
if (!result.response) throw new Error("This artifact could not be read.");
|
|
149
|
+
return result.response;
|
|
150
|
+
};
|
|
151
|
+
const probe = await load({ Range: "bytes=0-0" }, signal);
|
|
152
|
+
const range = /^bytes 0-0\/(\d+)$/u.exec(probe.headers.get("content-range") ?? "");
|
|
153
|
+
const empty = probe.status === 416 && probe.headers.get("content-range") === "bytes */0" || probe.status === 200 && probe.headers.get("content-length") === "0";
|
|
154
|
+
if (!empty && (probe.status !== 206 || !range)) throw new Error("This artifact does not support bounded downloads.");
|
|
155
|
+
const size = empty ? 0 : Number(range[1]);
|
|
156
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new Error("Invalid artifact size.");
|
|
157
|
+
if ((await probe.arrayBuffer()).byteLength !== (empty ? 0 : 1)) throw new Error("Invalid artifact response.");
|
|
158
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
159
|
+
const version = probe.headers.get("etag");
|
|
160
|
+
if (!version) throw new Error("The artifact has no version validator.");
|
|
161
|
+
return {
|
|
162
|
+
name: path.split("/").at(-1) ?? "artifact",
|
|
163
|
+
mimeType: probe.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream",
|
|
164
|
+
size,
|
|
165
|
+
signal,
|
|
166
|
+
read: async (offset, count, signal) => {
|
|
167
|
+
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(count) || offset < 0 || count < 1 || count > 1048576 || offset + count > size) throw new Error("Invalid download range.");
|
|
168
|
+
const end = offset + count - 1;
|
|
169
|
+
const response = await load({
|
|
170
|
+
Range: `bytes=${offset}-${end}`,
|
|
171
|
+
"If-Match": version
|
|
172
|
+
}, signal);
|
|
173
|
+
if (response.status !== 206 || response.headers.get("etag") !== version || response.headers.get("content-range") !== `bytes ${offset}-${end}/${size}`) throw new Error("The artifact changed or its range response is invalid.");
|
|
174
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
175
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
176
|
+
if (bytes.byteLength !== count) throw new Error("The artifact response is incomplete.");
|
|
177
|
+
return bytes;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/types/artifacts.ts
|
|
183
|
+
const artifactsChannelType = "artifacts";
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/artifactsStore.native.ts
|
|
186
|
+
const artifacts = defineSessionStore({
|
|
187
|
+
items: [],
|
|
188
|
+
loaded: false
|
|
189
|
+
});
|
|
190
|
+
const artifactsChannel = artifacts.channel({
|
|
191
|
+
channel: artifactsChannelType,
|
|
192
|
+
parse: parseArtifactSnapshot,
|
|
193
|
+
reduce: (_current, snapshot) => ({
|
|
194
|
+
...snapshot,
|
|
195
|
+
items: sortedArtifacts(snapshot.items),
|
|
196
|
+
loaded: true
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
/** An idle library remains reachable, without marking it as background work. */
|
|
200
|
+
const artifactActivitySource = {
|
|
201
|
+
subscribe: (listener) => {
|
|
202
|
+
const subscription = artifacts.store.subscribe(listener);
|
|
203
|
+
return () => subscription.unsubscribe();
|
|
204
|
+
},
|
|
205
|
+
isActive: () => false
|
|
206
|
+
};
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/artifactHooks.native.ts
|
|
209
|
+
/** A fresh grid read complements the live session channel and seeds a newly attached rail. */
|
|
210
|
+
function useArtifacts(sessionId, refreshOnMount = false) {
|
|
211
|
+
const snapshot = useSyncExternalStore(artifactActivitySource.subscribe, () => artifacts.select(artifacts.store.state, sessionId));
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
if (sessionId === null) return;
|
|
214
|
+
const before = artifacts.select(artifacts.store.state, sessionId);
|
|
215
|
+
if (before.loaded && !refreshOnMount) return;
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
fetchArtifacts(sessionId, controller.signal).then((next) => {
|
|
218
|
+
if (!controller.signal.aborted) artifacts.update(sessionId, (current) => current === before ? {
|
|
219
|
+
...next,
|
|
220
|
+
loaded: true
|
|
221
|
+
} : current);
|
|
222
|
+
}).catch((error) => {
|
|
223
|
+
if (!controller.signal.aborted) artifacts.update(sessionId, (current) => current === before ? {
|
|
224
|
+
...current,
|
|
225
|
+
loaded: true,
|
|
226
|
+
error: error instanceof Error ? error.message : String(error)
|
|
227
|
+
} : current);
|
|
228
|
+
});
|
|
229
|
+
return () => controller.abort();
|
|
230
|
+
}, [sessionId, refreshOnMount]);
|
|
231
|
+
return snapshot;
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/ArtifactList.native.tsx
|
|
235
|
+
/** Presentation only. The session store and guarded API belong to the containers. */
|
|
236
|
+
function ArtifactList({ items, onOpen, loading, error, grid = false }) {
|
|
237
|
+
return /* @__PURE__ */ jsxs(View, {
|
|
238
|
+
testID: grid ? "artifacts-grid" : "artifacts-recent",
|
|
239
|
+
style: styles$3.list,
|
|
240
|
+
children: [
|
|
241
|
+
error ? /* @__PURE__ */ jsx(Text, {
|
|
242
|
+
accessibilityRole: "alert",
|
|
243
|
+
style: styles$3.error,
|
|
244
|
+
children: error
|
|
245
|
+
}) : null,
|
|
246
|
+
loading ? /* @__PURE__ */ jsx(Text, {
|
|
247
|
+
style: styles$3.meta,
|
|
248
|
+
children: "Loading artifacts..."
|
|
249
|
+
}) : null,
|
|
250
|
+
!loading && !error && items.length === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
251
|
+
style: styles$3.meta,
|
|
252
|
+
children: "Documents saved for this session will appear here."
|
|
253
|
+
}) : null,
|
|
254
|
+
items.map((item) => /* @__PURE__ */ jsxs(Button, {
|
|
255
|
+
variant: grid ? "outline" : "ghost",
|
|
256
|
+
size: "card",
|
|
257
|
+
accessibilityLabel: `Open ${item.path}`,
|
|
258
|
+
testID: `artifact-${item.path}`,
|
|
259
|
+
onPress: () => onOpen(item.path),
|
|
260
|
+
style: styles$3.item,
|
|
261
|
+
children: [/* @__PURE__ */ jsx(FolderIcon, {
|
|
262
|
+
size: 18,
|
|
263
|
+
color: "dim"
|
|
264
|
+
}), /* @__PURE__ */ jsxs(View, {
|
|
265
|
+
style: styles$3.label,
|
|
266
|
+
children: [
|
|
267
|
+
/* @__PURE__ */ jsx(Text, {
|
|
268
|
+
numberOfLines: 2,
|
|
269
|
+
style: styles$3.name,
|
|
270
|
+
children: item.name
|
|
271
|
+
}),
|
|
272
|
+
grid || item.path !== item.name ? /* @__PURE__ */ jsx(Text, {
|
|
273
|
+
numberOfLines: 2,
|
|
274
|
+
style: styles$3.path,
|
|
275
|
+
children: item.path
|
|
276
|
+
}) : null,
|
|
277
|
+
grid ? /* @__PURE__ */ jsxs(Text, {
|
|
278
|
+
style: styles$3.meta,
|
|
279
|
+
children: [item.size.toLocaleString("en-US"), " bytes"]
|
|
280
|
+
}) : null
|
|
281
|
+
]
|
|
282
|
+
})]
|
|
283
|
+
}, item.path))
|
|
284
|
+
]
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const styles$3 = StyleSheet.create((theme) => ({
|
|
288
|
+
list: {
|
|
289
|
+
gap: theme.spacing(2),
|
|
290
|
+
uni__dependencies: [0]
|
|
291
|
+
},
|
|
292
|
+
item: {
|
|
293
|
+
minHeight: 44,
|
|
294
|
+
justifyContent: "flex-start",
|
|
295
|
+
alignItems: "center",
|
|
296
|
+
gap: theme.spacing(2),
|
|
297
|
+
uni__dependencies: [0]
|
|
298
|
+
},
|
|
299
|
+
label: {
|
|
300
|
+
flex: 1,
|
|
301
|
+
gap: theme.spacing(1),
|
|
302
|
+
minWidth: 0,
|
|
303
|
+
uni__dependencies: [0]
|
|
304
|
+
},
|
|
305
|
+
name: {
|
|
306
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
307
|
+
fontWeight: "600",
|
|
308
|
+
fontSize: theme.fontSize.base,
|
|
309
|
+
color: theme.colors.text,
|
|
310
|
+
uni__dependencies: [0]
|
|
311
|
+
},
|
|
312
|
+
path: {
|
|
313
|
+
fontFamily: fontOf(theme.fontFamily.mono),
|
|
314
|
+
fontSize: theme.fontSize.sm,
|
|
315
|
+
color: theme.colors.dim,
|
|
316
|
+
uni__dependencies: [0]
|
|
317
|
+
},
|
|
318
|
+
meta: {
|
|
319
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
320
|
+
fontSize: theme.fontSize.sm,
|
|
321
|
+
color: theme.colors.dim,
|
|
322
|
+
uni__dependencies: [0]
|
|
323
|
+
},
|
|
324
|
+
error: {
|
|
325
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
326
|
+
fontSize: theme.fontSize.sm,
|
|
327
|
+
color: theme.colors.red,
|
|
328
|
+
uni__dependencies: [0]
|
|
329
|
+
}
|
|
330
|
+
}));
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/ArtifactPreview.native.tsx
|
|
333
|
+
/** The host receives only content fetched through the artifact's bounded route. */
|
|
334
|
+
function ArtifactPreview({ preview, sessionId, renderFilePreview, renderMediaPreview, media, mediaError, onOpen }) {
|
|
335
|
+
const mode = artifactPreviewMode(preview.path);
|
|
336
|
+
if (mode === "media") {
|
|
337
|
+
if (mediaError) return /* @__PURE__ */ jsx(Text, {
|
|
338
|
+
accessibilityRole: "alert",
|
|
339
|
+
style: styles$2.error,
|
|
340
|
+
children: mediaError
|
|
341
|
+
});
|
|
342
|
+
if (!renderMediaPreview) return /* @__PURE__ */ jsx(Text, {
|
|
343
|
+
style: styles$2.meta,
|
|
344
|
+
children: "This host cannot display this media preview."
|
|
345
|
+
});
|
|
346
|
+
if (!media) return /* @__PURE__ */ jsx(Text, {
|
|
347
|
+
style: styles$2.meta,
|
|
348
|
+
children: "Loading media..."
|
|
349
|
+
});
|
|
350
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderMediaPreview({
|
|
351
|
+
key: `artifact-media-${sessionId}-${preview.path}`,
|
|
352
|
+
title: preview.path,
|
|
353
|
+
path: preview.path,
|
|
354
|
+
data: media.data,
|
|
355
|
+
mimeType: media.mimeType
|
|
356
|
+
}) });
|
|
357
|
+
}
|
|
358
|
+
if (preview.unavailable) return /* @__PURE__ */ jsx(Text, {
|
|
359
|
+
style: styles$2.meta,
|
|
360
|
+
children: preview.reason ?? "This artifact cannot be previewed."
|
|
361
|
+
});
|
|
362
|
+
if (mode === "html") return renderFilePreview ? /* @__PURE__ */ jsx(View, {
|
|
363
|
+
style: styles$2.html,
|
|
364
|
+
children: renderFilePreview({
|
|
365
|
+
key: `artifact-html-${sessionId}-${preview.path}`,
|
|
366
|
+
html: preview.content,
|
|
367
|
+
title: preview.path
|
|
368
|
+
})
|
|
369
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
370
|
+
style: styles$2.meta,
|
|
371
|
+
children: "This host cannot display HTML previews."
|
|
372
|
+
});
|
|
373
|
+
if (mode === "markdown") return /* @__PURE__ */ jsx(Markdown, {
|
|
374
|
+
text: preview.content,
|
|
375
|
+
onFileLink: (path) => {
|
|
376
|
+
try {
|
|
377
|
+
const resolved = artifactAssetPath(preview.path, path);
|
|
378
|
+
return () => onOpen(resolved);
|
|
379
|
+
} catch {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
if (mode === "code") return /* @__PURE__ */ jsx(CodeBlock, {
|
|
385
|
+
text: preview.content,
|
|
386
|
+
language: preview.path.split(".").at(-1)
|
|
387
|
+
});
|
|
388
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
389
|
+
selectable: true,
|
|
390
|
+
style: styles$2.text,
|
|
391
|
+
children: preview.content
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const styles$2 = StyleSheet.create((theme) => ({
|
|
395
|
+
html: {
|
|
396
|
+
minHeight: theme.spacing(90),
|
|
397
|
+
uni__dependencies: [0]
|
|
398
|
+
},
|
|
399
|
+
text: {
|
|
400
|
+
fontFamily: fontOf(theme.fontFamily.mono),
|
|
401
|
+
fontSize: theme.fontSize.sm,
|
|
402
|
+
lineHeight: theme.lineHeight.sm,
|
|
403
|
+
color: theme.colors.text,
|
|
404
|
+
uni__dependencies: [0]
|
|
405
|
+
},
|
|
406
|
+
meta: {
|
|
407
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
408
|
+
fontSize: theme.fontSize.sm,
|
|
409
|
+
color: theme.colors.dim,
|
|
410
|
+
uni__dependencies: [0]
|
|
411
|
+
},
|
|
412
|
+
error: {
|
|
413
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
414
|
+
fontSize: theme.fontSize.sm,
|
|
415
|
+
color: theme.colors.red,
|
|
416
|
+
uni__dependencies: [0]
|
|
417
|
+
}
|
|
418
|
+
}));
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/ArtifactPanel.native.tsx
|
|
421
|
+
function ArtifactPanel({ path, ...props }) {
|
|
422
|
+
const item = useArtifacts(props.sessionId).items.find((item) => item.path === path);
|
|
423
|
+
const version = `${item?.modifiedAt ?? ""}:${item?.size ?? ""}`;
|
|
424
|
+
const downloadKey = `${props.sessionId}:${path}`;
|
|
425
|
+
const key = `${downloadKey}:${version}`;
|
|
426
|
+
const [loaded, setLoaded] = useState();
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
if (props.sessionId === null) return;
|
|
429
|
+
const sessionId = props.sessionId;
|
|
430
|
+
const controller = new AbortController();
|
|
431
|
+
fetchArtifactPreview(sessionId, path, controller.signal).then(async (preview) => {
|
|
432
|
+
if (controller.signal.aborted) return;
|
|
433
|
+
setLoaded({
|
|
434
|
+
key,
|
|
435
|
+
preview
|
|
436
|
+
});
|
|
437
|
+
if (artifactPreviewMode(path) !== "media" || !props.renderMediaPreview) return;
|
|
438
|
+
try {
|
|
439
|
+
const media = await fetchArtifactMedia(sessionId, path, controller.signal);
|
|
440
|
+
if (!controller.signal.aborted) setLoaded({
|
|
441
|
+
key,
|
|
442
|
+
preview,
|
|
443
|
+
media
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (!controller.signal.aborted) setLoaded({
|
|
447
|
+
key,
|
|
448
|
+
preview,
|
|
449
|
+
mediaError: error instanceof Error ? error.message : String(error)
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}).catch((error) => {
|
|
453
|
+
if (!controller.signal.aborted) setLoaded({
|
|
454
|
+
key,
|
|
455
|
+
error: error instanceof Error ? error.message : String(error)
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
return () => controller.abort();
|
|
459
|
+
}, [
|
|
460
|
+
props.sessionId,
|
|
461
|
+
path,
|
|
462
|
+
key,
|
|
463
|
+
props.renderMediaPreview
|
|
464
|
+
]);
|
|
465
|
+
const current = loaded?.key === key ? loaded : void 0;
|
|
466
|
+
const downloadController = useRef(void 0);
|
|
467
|
+
const [download, setDownload] = useState();
|
|
468
|
+
useEffect(() => () => {
|
|
469
|
+
downloadController.current?.abort();
|
|
470
|
+
downloadController.current = void 0;
|
|
471
|
+
}, [props.sessionId, path]);
|
|
472
|
+
const saving = download?.key === downloadKey && download.saving === true;
|
|
473
|
+
const save = async () => {
|
|
474
|
+
if (props.sessionId === null || !props.nativeSaveFile || downloadController.current) return;
|
|
475
|
+
const controller = new AbortController();
|
|
476
|
+
downloadController.current = controller;
|
|
477
|
+
setDownload({
|
|
478
|
+
key: downloadKey,
|
|
479
|
+
saving: true
|
|
480
|
+
});
|
|
481
|
+
try {
|
|
482
|
+
const file = await prepareArtifactSave(props.sessionId, path, controller.signal);
|
|
483
|
+
if (controller.signal.aborted || downloadController.current !== controller) return;
|
|
484
|
+
await props.nativeSaveFile(file);
|
|
485
|
+
if (!controller.signal.aborted && downloadController.current === controller) setDownload({
|
|
486
|
+
key: downloadKey,
|
|
487
|
+
note: "Saved."
|
|
488
|
+
});
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (!controller.signal.aborted && downloadController.current === controller) setDownload({
|
|
491
|
+
key: downloadKey,
|
|
492
|
+
note: error instanceof Error ? error.message : String(error)
|
|
493
|
+
});
|
|
494
|
+
} finally {
|
|
495
|
+
if (downloadController.current === controller) downloadController.current = void 0;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
return /* @__PURE__ */ jsxs(ScrollView, {
|
|
499
|
+
contentContainerStyle: styles$1.content,
|
|
500
|
+
children: [
|
|
501
|
+
/* @__PURE__ */ jsx(Text, {
|
|
502
|
+
selectable: true,
|
|
503
|
+
style: styles$1.path,
|
|
504
|
+
children: path
|
|
505
|
+
}),
|
|
506
|
+
props.nativeSaveFile ? /* @__PURE__ */ jsxs(View, {
|
|
507
|
+
style: styles$1.actions,
|
|
508
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
509
|
+
variant: "outline",
|
|
510
|
+
disabled: saving || props.sessionId === null,
|
|
511
|
+
onPress: () => void save(),
|
|
512
|
+
children: "Download"
|
|
513
|
+
}), saving ? /* @__PURE__ */ jsx(Button, {
|
|
514
|
+
variant: "ghost",
|
|
515
|
+
onPress: () => {
|
|
516
|
+
downloadController.current?.abort();
|
|
517
|
+
downloadController.current = void 0;
|
|
518
|
+
setDownload({
|
|
519
|
+
key: downloadKey,
|
|
520
|
+
note: "Download cancelled."
|
|
521
|
+
});
|
|
522
|
+
},
|
|
523
|
+
children: "Cancel download"
|
|
524
|
+
}) : null]
|
|
525
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
526
|
+
style: styles$1.meta,
|
|
527
|
+
children: "This host cannot save artifact downloads."
|
|
528
|
+
}),
|
|
529
|
+
download?.key === downloadKey && download.note ? /* @__PURE__ */ jsx(Text, {
|
|
530
|
+
accessibilityLiveRegion: "polite",
|
|
531
|
+
style: styles$1.meta,
|
|
532
|
+
children: download.note
|
|
533
|
+
}) : null,
|
|
534
|
+
props.sessionId === null ? /* @__PURE__ */ jsx(Text, {
|
|
535
|
+
accessibilityRole: "alert",
|
|
536
|
+
style: styles$1.error,
|
|
537
|
+
children: "The session is unavailable."
|
|
538
|
+
}) : current?.error ? /* @__PURE__ */ jsx(Text, {
|
|
539
|
+
accessibilityRole: "alert",
|
|
540
|
+
style: styles$1.error,
|
|
541
|
+
children: current.error
|
|
542
|
+
}) : current?.preview ? /* @__PURE__ */ jsx(ArtifactPreview, {
|
|
543
|
+
preview: current.preview,
|
|
544
|
+
sessionId: props.sessionId,
|
|
545
|
+
renderFilePreview: props.renderFilePreview,
|
|
546
|
+
renderMediaPreview: props.renderMediaPreview,
|
|
547
|
+
media: current.media,
|
|
548
|
+
mediaError: current.mediaError,
|
|
549
|
+
onOpen: (path) => {
|
|
550
|
+
if (props.sessionId !== null) props.openTransientTab(artifactTab(props.sessionId, path));
|
|
551
|
+
}
|
|
552
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
553
|
+
style: styles$1.meta,
|
|
554
|
+
children: "Reading this artifact..."
|
|
555
|
+
})
|
|
556
|
+
]
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
function artifactTab(sessionId, path) {
|
|
560
|
+
return {
|
|
561
|
+
id: artifactTabId(sessionId, path),
|
|
562
|
+
label: path.split("/").at(-1) ?? path,
|
|
563
|
+
panel: (props) => /* @__PURE__ */ jsx(ArtifactPanel, {
|
|
564
|
+
...props,
|
|
565
|
+
sessionId,
|
|
566
|
+
path
|
|
567
|
+
})
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const styles$1 = StyleSheet.create((theme) => ({
|
|
571
|
+
content: {
|
|
572
|
+
padding: theme.spacing(4),
|
|
573
|
+
gap: theme.spacing(3),
|
|
574
|
+
uni__dependencies: [0]
|
|
575
|
+
},
|
|
576
|
+
actions: {
|
|
577
|
+
flexDirection: "row",
|
|
578
|
+
flexWrap: "wrap",
|
|
579
|
+
gap: theme.spacing(2),
|
|
580
|
+
uni__dependencies: [0]
|
|
581
|
+
},
|
|
582
|
+
path: {
|
|
583
|
+
fontFamily: fontOf(theme.fontFamily.mono),
|
|
584
|
+
fontSize: theme.fontSize.sm,
|
|
585
|
+
color: theme.colors.dim,
|
|
586
|
+
uni__dependencies: [0]
|
|
587
|
+
},
|
|
588
|
+
meta: {
|
|
589
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
590
|
+
fontSize: theme.fontSize.sm,
|
|
591
|
+
color: theme.colors.dim,
|
|
592
|
+
uni__dependencies: [0]
|
|
593
|
+
},
|
|
594
|
+
error: {
|
|
595
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
596
|
+
fontSize: theme.fontSize.sm,
|
|
597
|
+
color: theme.colors.red,
|
|
598
|
+
uni__dependencies: [0]
|
|
599
|
+
}
|
|
600
|
+
}));
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/ArtifactsPanel.native.tsx
|
|
603
|
+
function ArtifactsPanel(props) {
|
|
604
|
+
const snapshot = useArtifacts(props.sessionId, true);
|
|
605
|
+
return /* @__PURE__ */ jsxs(ScrollView, {
|
|
606
|
+
contentContainerStyle: styles.content,
|
|
607
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
608
|
+
accessibilityRole: "header",
|
|
609
|
+
style: styles.heading,
|
|
610
|
+
children: "Artifacts"
|
|
611
|
+
}), /* @__PURE__ */ jsx(ArtifactList, {
|
|
612
|
+
grid: true,
|
|
613
|
+
items: snapshot.items,
|
|
614
|
+
loading: !snapshot.loaded,
|
|
615
|
+
error: props.sessionId === null ? "The session is unavailable." : snapshot.error,
|
|
616
|
+
onOpen: (path) => {
|
|
617
|
+
if (props.sessionId !== null) props.openTransientTab(artifactTab(props.sessionId, path));
|
|
618
|
+
}
|
|
619
|
+
})]
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function ArtifactsActivitySection(props) {
|
|
623
|
+
const snapshot = useArtifacts(props.sessionId);
|
|
624
|
+
return /* @__PURE__ */ jsx(ArtifactList, {
|
|
625
|
+
items: snapshot.items.slice(0, 3),
|
|
626
|
+
loading: !snapshot.loaded,
|
|
627
|
+
error: props.sessionId === null ? "The session is unavailable." : snapshot.error,
|
|
628
|
+
onOpen: (path) => {
|
|
629
|
+
if (props.sessionId !== null) props.openTransientTab(artifactTab(props.sessionId, path));
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
function artifactsTab() {
|
|
634
|
+
return {
|
|
635
|
+
id: "artifacts",
|
|
636
|
+
label: "Artifacts",
|
|
637
|
+
panel: ArtifactsPanel
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const styles = StyleSheet.create((theme) => ({
|
|
641
|
+
content: {
|
|
642
|
+
padding: theme.spacing(4),
|
|
643
|
+
gap: theme.spacing(4),
|
|
644
|
+
uni__dependencies: [0]
|
|
645
|
+
},
|
|
646
|
+
heading: {
|
|
647
|
+
fontFamily: fontOf(theme.fontFamily.sans),
|
|
648
|
+
fontWeight: "600",
|
|
649
|
+
fontSize: theme.fontSize.lg,
|
|
650
|
+
color: theme.colors.hi,
|
|
651
|
+
uni__dependencies: [0]
|
|
652
|
+
}
|
|
653
|
+
}));
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/extensions/workspaces/sessions/(frontend)/activity-group/artifacts.native.ts
|
|
656
|
+
var artifacts_native_default$1 = defineActivityGroup({
|
|
657
|
+
name: "artifacts",
|
|
658
|
+
keys: "",
|
|
659
|
+
activeSource: artifactActivitySource,
|
|
660
|
+
marksBackgroundWork: false,
|
|
661
|
+
transientTab: artifactsTab,
|
|
662
|
+
order: 15
|
|
663
|
+
});
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/artifacts.native.ts
|
|
666
|
+
var artifacts_native_default = defineChannelFile(artifactsChannel);
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/Artifacts.activity.artifacts.native.tsx
|
|
669
|
+
var Artifacts_activity_artifacts_native_default = defineFill({
|
|
670
|
+
slot: "activity.artifacts",
|
|
671
|
+
id: "artifacts",
|
|
672
|
+
component: ArtifactsActivitySection
|
|
673
|
+
});
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region generated/native.ts
|
|
676
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
677
|
+
...identity,
|
|
678
|
+
...value
|
|
679
|
+
};
|
|
680
|
+
const nativePlugin = defineNativePlugin({
|
|
681
|
+
id: "artifacts",
|
|
682
|
+
session: {
|
|
683
|
+
get activityGroups() {
|
|
684
|
+
return [via({ name: "artifacts" }, artifacts_native_default$1)];
|
|
685
|
+
},
|
|
686
|
+
get channels() {
|
|
687
|
+
return [via({ channel: "artifacts" }, artifacts_native_default)];
|
|
688
|
+
},
|
|
689
|
+
get fills() {
|
|
690
|
+
return [via({
|
|
691
|
+
slot: "activity.artifacts",
|
|
692
|
+
id: "artifacts"
|
|
693
|
+
}, Artifacts_activity_artifacts_native_default)];
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
//#endregion
|
|
698
|
+
export { nativePlugin };
|
|
699
|
+
|
|
700
|
+
//# sourceMappingURL=native.mjs.map
|