dsh-files-native 0.1.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/lib/client.js ADDED
@@ -0,0 +1,1118 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-files-native", factory: (require) => {
2
+ var module = { exports: {} };
3
+ var exports = module.exports;
4
+ "use strict";
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name2 in all)
11
+ __defProp(target, name2, { get: all[name2], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+
23
+ // src/client.ts
24
+ var client_exports = {};
25
+ __export(client_exports, {
26
+ apply: () => apply,
27
+ inject: () => inject,
28
+ name: () => name
29
+ });
30
+ module.exports = __toCommonJS(client_exports);
31
+ var import_react8 = require("react");
32
+
33
+ // src/rail.tsx
34
+ var import_react2 = require("react");
35
+
36
+ // src/lib.ts
37
+ var NATIVE_IMAGE_TYPES = /* @__PURE__ */ new Set([
38
+ "image/png",
39
+ "image/jpeg",
40
+ "image/webp",
41
+ "image/gif"
42
+ ]);
43
+ var NATIVE_IMAGE_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp", "gif"]);
44
+ var MAX_FILE_BYTES = 50 * 1024 * 1024;
45
+ var MAX_FILES_PER_BATCH = 20;
46
+ var UPLOAD_DIR = ".dsh-uploads";
47
+ function isNativeImage(file) {
48
+ const type = (file.type ?? "").toLowerCase();
49
+ if (NATIVE_IMAGE_TYPES.has(type)) return true;
50
+ if (type !== "" && type !== "application/octet-stream") return false;
51
+ const ext = extOf(file.name ?? "");
52
+ return NATIVE_IMAGE_EXT.has(ext.toLowerCase());
53
+ }
54
+ function extOf(name2) {
55
+ const base = name2.replace(/^.*[/\\]/, "");
56
+ const dot = base.lastIndexOf(".");
57
+ if (dot <= 0 || dot === base.length - 1) return "FILE";
58
+ return base.slice(dot + 1).slice(0, 4).toUpperCase();
59
+ }
60
+ function formatSize(bytes) {
61
+ if (bytes < 1024) return `${bytes} B`;
62
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
63
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
64
+ }
65
+ function isSafeRelPath(relPath) {
66
+ if (relPath === "" || relPath.startsWith("/") || relPath.startsWith("\\")) return false;
67
+ const parts = relPath.split(/[/\\]/);
68
+ if (parts[0] !== UPLOAD_DIR) return false;
69
+ return parts.every((part) => part !== "" && part !== "." && part !== "..");
70
+ }
71
+ function splitIntake(files) {
72
+ const images = [];
73
+ const others = [];
74
+ for (const file of files) {
75
+ if (isNativeImage(file)) images.push(file);
76
+ else others.push(file);
77
+ }
78
+ return { images, others };
79
+ }
80
+ var NOTICE_LINE = /^- (.+?) — path="([^"]+)" size=(\d+)(?: type="([^"]*)")?/;
81
+ function parseNoticeFiles(text) {
82
+ const files = [];
83
+ for (const line of text.split("\n")) {
84
+ const match = NOTICE_LINE.exec(line.trim());
85
+ if (match === null) continue;
86
+ const relPath = match[2] ?? "";
87
+ if (!isSafeRelPath(relPath)) continue;
88
+ files.push({
89
+ name: match[1] ?? "file",
90
+ relPath,
91
+ size: Number(match[3] ?? 0),
92
+ mediaType: match[4] || "application/octet-stream"
93
+ });
94
+ }
95
+ return files;
96
+ }
97
+ function contentText(content) {
98
+ if (typeof content === "string") return content;
99
+ if (!Array.isArray(content)) return "";
100
+ const parts = [];
101
+ for (const block of content) {
102
+ if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
103
+ parts.push(block.text);
104
+ }
105
+ }
106
+ return parts.join("\n");
107
+ }
108
+
109
+ // src/store.ts
110
+ var items = [];
111
+ var sentBySession = /* @__PURE__ */ new Map();
112
+ var sentByTurn = /* @__PURE__ */ new Map();
113
+ var listeners = /* @__PURE__ */ new Set();
114
+ var generation = 0;
115
+ function emit() {
116
+ generation += 1;
117
+ for (const fn of [...listeners]) fn();
118
+ }
119
+ function version() {
120
+ return generation;
121
+ }
122
+ function subscribe(fn) {
123
+ listeners.add(fn);
124
+ return () => {
125
+ listeners.delete(fn);
126
+ };
127
+ }
128
+ function pending() {
129
+ return items.slice();
130
+ }
131
+ function add(sessionId, item) {
132
+ items.push({ ...item, sessionId: item.sessionId || sessionId });
133
+ emit();
134
+ }
135
+ function patch(sessionId, id, update) {
136
+ const index = items.findIndex((item) => item.id === id);
137
+ if (index < 0) return;
138
+ const cur = items[index];
139
+ items[index] = { ...cur, ...update, sessionId: update.sessionId ?? cur.sessionId ?? sessionId };
140
+ emit();
141
+ }
142
+ function remove(_sessionId, id) {
143
+ const index = items.findIndex((item) => item.id === id);
144
+ if (index < 0) return void 0;
145
+ const [found] = items.splice(index, 1);
146
+ emit();
147
+ return found;
148
+ }
149
+ function doneFiles(sessionId) {
150
+ const mine = sessionId === "" ? items : items.filter((item) => !item.sessionId || item.sessionId === sessionId);
151
+ return mine.filter((item) => item.status === "done");
152
+ }
153
+ function archiveSent(sessionId) {
154
+ const files = doneFiles(sessionId);
155
+ if (files.length > 0) sentBySession.set(sessionId, files);
156
+ for (let i = items.length - 1; i >= 0; i -= 1) {
157
+ const item = items[i];
158
+ if (item.status !== "done") continue;
159
+ if (sessionId !== "" && item.sessionId && item.sessionId !== sessionId) continue;
160
+ items.splice(i, 1);
161
+ }
162
+ if (files.length > 0) emit();
163
+ return files;
164
+ }
165
+ function archiveIfNoticeMatches(sessionId, notice) {
166
+ if (notice.length === 0) return [];
167
+ const pendingDone = doneFiles(sessionId);
168
+ if (pendingDone.length === 0) return [];
169
+ const paths = new Set(notice.map((file) => file.relPath));
170
+ if (!pendingDone.some((file) => paths.has(file.relPath))) return [];
171
+ return archiveSent(sessionId);
172
+ }
173
+ function lastSent(sessionId) {
174
+ return sentBySession.get(sessionId) ?? [];
175
+ }
176
+ function filesForTurn(sessionId, turn) {
177
+ return sentByTurn.get(`${sessionId}:${turn}`) ?? lastSent(sessionId);
178
+ }
179
+
180
+ // src/styles.ts
181
+ var FR_CSS = `
182
+ .fr-rail,.fr-rail *{box-sizing:border-box}
183
+ [data-file-native-host]{flex:none;min-width:0;overflow:visible;padding:0}
184
+ [data-file-native-host]:empty,[data-file-native-host]:not(:has(.fr-rail)){display:none!important}
185
+ .fr-rail{min-width:0;position:relative;padding:4px 12px 0;overflow:visible;flex:none}
186
+ .fr-row{scrollbar-width:none;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);gap:10px;display:flex;overflow-x:auto;overflow-y:hidden;padding:0;align-items:center;min-height:64px}
187
+ .fr-row::-webkit-scrollbar{display:none}
188
+ .fr-item{flex:none;height:64px;position:relative}
189
+ .fr-item-image{flex:0 0 64px;width:64px}
190
+ .fr-thumb{border:1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l4));
191
+ background:var(--dsw-alias-interactive-bg-hover);cursor:zoom-in;border-radius:16px;width:64px;height:64px;
192
+ padding:0;overflow:hidden}
193
+ .fr-thumb img{object-fit:cover;width:100%;height:100%;display:block}
194
+ .fr-card{border:1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l4));
195
+ background:var(--dsw-alias-interactive-bg-hover);border-radius:16px;height:64px;min-width:168px;max-width:240px;
196
+ padding:8px 28px 8px 10px;display:flex;align-items:center;gap:10px;cursor:default}
197
+ .fr-card[data-status="error"]{border-color:var(--dsw-alias-state-danger-primary, #d94c4c)}
198
+ .fr-ext{font:var(--dsw-font-xxxs-11, 11px/14px ui-sans-serif,system-ui);font-weight:700;letter-spacing:.3px;
199
+ color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2, var(--dsw-alias-border-l4));
200
+ border-radius:8px;padding:6px 7px;line-height:1;flex:none;min-width:36px;text-align:center}
201
+ .fr-copy{min-width:0;display:flex;flex-direction:column;gap:2px}
202
+ .fr-name{font:var(--dsw-font-xs-13, 13px/18px ui-sans-serif,system-ui);color:var(--dsw-alias-label-primary);
203
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:170px}
204
+ .fr-meta{font:var(--dsw-font-xxxs-11, 11px/14px ui-sans-serif,system-ui);color:var(--dsw-alias-label-caption)}
205
+ .fr-remove{z-index:1;background:var(--dsw-alias-button-contrast-fill);width:18px;height:18px;
206
+ color:var(--dsw-alias-label-primary-inverted);cursor:pointer;opacity:0;border:none;border-radius:50%;
207
+ place-items:center;padding:0;transition:opacity .2s ease-in-out;display:grid;position:absolute;top:4px;right:4px}
208
+ .fr-item:hover .fr-remove,.fr-remove:focus-visible{opacity:1}
209
+ @media (pointer:coarse){.fr-remove{opacity:1}}
210
+ @media (prefers-reduced-motion:reduce){.fr-remove{transition:none}}
211
+ .fr-arrow{z-index:2;border:1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l4));
212
+ background:var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-1));width:24px;height:24px;
213
+ color:var(--dsw-alias-label-secondary);box-shadow:var(--dsw-shadow-lv2, 0 8px 24px rgba(16,24,40,.08));
214
+ cursor:pointer;border-radius:999px;place-items:center;padding:0;display:grid;position:absolute;top:50%;transform:translateY(-50%)}
215
+ .fr-arrow:hover{background:var(--dsw-alias-interactive-bg-hover-solid, var(--dsw-alias-interactive-bg-hover))}
216
+ .fr-arrow-left{left:4px}
217
+ .fr-arrow-right{right:4px}
218
+ .fr-mask{z-index:1000;pointer-events:none;background-color:var(--dsw-alias-bg-mask-drop, rgba(0,0,0,.45));
219
+ backdrop-filter:blur(10px);justify-content:center;align-items:center;animation:.16s ease-out fr-fade-in;
220
+ display:flex;position:fixed;inset:0}
221
+ @keyframes fr-fade-in{0%{opacity:0}to{opacity:1}}
222
+ @media (prefers-reduced-motion:reduce){.fr-mask{animation:none}}
223
+ .fr-wrap{color:var(--dsw-alias-label-primary);text-align:center;flex-direction:column;align-items:center;
224
+ margin-top:-3%;padding:0 40px;display:flex}
225
+ .fr-title{font:var(--dsw-font-l-20, 600 20px/28px system-ui);margin-top:16px}
226
+ .fr-desc{font:var(--dsw-font-s-14, 14px/20px system-ui);color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;margin-top:16px}
227
+ .fr-illust{width:115px;height:84px}
228
+ .fr-pick{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;
229
+ background:transparent;color:var(--dsw-alias-label-secondary);border-radius:8px;cursor:pointer;padding:0}
230
+ .fr-pick:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
231
+ .fr-hidden{display:none}
232
+ .fr-lightbox{z-index:1100;position:fixed;inset:0;background:rgba(0,0,0,.72);display:grid;place-items:center;cursor:zoom-out}
233
+ .fr-lightbox img{max-width:min(92vw,1200px);max-height:88vh;border-radius:12px;box-shadow:var(--dsw-shadow-lv2)}
234
+ .fr-tail{display:grid;grid-template-columns:max-content minmax(0,1fr);align-items:center;gap:4px 8px;margin-top:16px;
235
+ font-size:13px;line-height:22px}
236
+ .fr-tail-label{color:var(--dsw-alias-label-tertiary);grid-column:1}
237
+ .fr-tail-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;min-width:0;grid-column:2}
238
+ .fr-chip{text-overflow:ellipsis;white-space:nowrap;background:var(--dsw-alias-interactive-bg-hover);max-width:320px;
239
+ color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;border:none;border-radius:6px;flex:none;
240
+ margin:0;padding:0 8px;overflow:hidden}
241
+ .fr-chip:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}
242
+ .fr-more{white-space:nowrap;color:var(--dsw-alias-label-tertiary);flex:none}
243
+ .fr-msg{display:flex;flex-direction:column;align-items:flex-end;gap:6px;min-width:0}
244
+ .fr-msg-row,.fr-msg-row-inner{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;width:100%}
245
+ [data-file-native-msg]{flex:none;min-width:0;width:100%}
246
+ [data-file-native-anchor]{display:none!important}
247
+ [data-chat-flow-kind="context"]:has([data-file-native-anchor]){display:none!important;height:0;margin:0;padding:0;overflow:hidden}
248
+ .fr-msg-card{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2-darkmode-thin, var(--dsw-alias-border-l4));
249
+ background:var(--dsw-alias-interactive-bg-hover);border-radius:16px;height:64px;min-width:168px;max-width:240px;
250
+ padding:8px 12px;display:flex;align-items:center;gap:10px;cursor:pointer;text-align:left;color:inherit;font:inherit}
251
+ .fr-msg-card:hover{background:var(--dsw-alias-interactive-bg-hover-solid, var(--dsw-alias-interactive-bg-hover))}
252
+ .fr-ctx{min-width:0}
253
+ .fr-ctx-head{width:100%;min-width:0;height:24px;color:inherit;font:inherit;text-align:left;background:0 0;border:none;
254
+ border-radius:6px;align-items:center;padding:0;display:flex;cursor:pointer;gap:8px}
255
+ .fr-ctx-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
256
+ .fr-ctx-title{color:var(--dsw-alias-label-primary-dimmed, var(--dsw-alias-label-secondary));flex:none;font-size:14px;line-height:24px}
257
+ .fr-ctx-src,.fr-ctx-sum{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;
258
+ font-size:14px;line-height:24px;overflow:hidden}
259
+ .fr-ctx-src{flex:none;max-width:180px}
260
+ .fr-ctx-sum{flex:auto}
261
+ .fr-ctx-body{box-sizing:border-box;background:var(--dsw-alias-markdown-code-block);width:calc(100% - 22px);max-height:141px;
262
+ color:var(--dsw-alias-label-tertiary);font:400 11px/16px var(--ds-font-family-code, ui-monospace,monospace);
263
+ border:none;border-radius:8px;margin:4px 0 0 22px;padding:10px 16px 12px 12px;overflow:auto;white-space:pre-wrap}
264
+ `;
265
+ var injected = false;
266
+ function ensureStyles() {
267
+ if (typeof document === "undefined") return;
268
+ let el = document.getElementById("file-native-styles");
269
+ if (el === null) {
270
+ el = document.createElement("style");
271
+ el.id = "file-native-styles";
272
+ document.head.appendChild(el);
273
+ }
274
+ if (el.textContent !== FR_CSS) el.textContent = FR_CSS;
275
+ injected = true;
276
+ }
277
+
278
+ // src/icons.tsx
279
+ var import_react = require("react");
280
+ function IconPaperclip({ size = 16 }) {
281
+ return (0, import_react.createElement)(
282
+ "svg",
283
+ {
284
+ width: size,
285
+ height: size,
286
+ viewBox: "0 0 16 16",
287
+ fill: "none",
288
+ "aria-hidden": true
289
+ },
290
+ (0, import_react.createElement)("path", {
291
+ d: "M9.2 4.4 4.55 9.05a2.4 2.4 0 1 0 3.4 3.4l5.15-5.15a3.6 3.6 0 0 0-5.1-5.1L3.15 7.05",
292
+ stroke: "currentColor",
293
+ strokeWidth: "1.5",
294
+ strokeLinecap: "round",
295
+ strokeLinejoin: "round"
296
+ })
297
+ );
298
+ }
299
+ function IconChevronLeft({ size = 14 }) {
300
+ return (0, import_react.createElement)(
301
+ "svg",
302
+ { width: size, height: size, viewBox: "0 0 16 16", fill: "none", "aria-hidden": true },
303
+ (0, import_react.createElement)("path", { d: "M10 3.5 5.5 8 10 12.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })
304
+ );
305
+ }
306
+ function IconChevronRight({ size = 14 }) {
307
+ return (0, import_react.createElement)(
308
+ "svg",
309
+ { width: size, height: size, viewBox: "0 0 16 16", fill: "none", "aria-hidden": true },
310
+ (0, import_react.createElement)("path", { d: "M6 3.5 10.5 8 6 12.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })
311
+ );
312
+ }
313
+ function IconClose({ size = 12 }) {
314
+ return (0, import_react.createElement)(
315
+ "svg",
316
+ { width: size, height: size, viewBox: "0 0 12 12", fill: "none", "aria-hidden": true },
317
+ (0, import_react.createElement)("path", { d: "M3 3l6 6M9 3 3 9", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })
318
+ );
319
+ }
320
+ function UploadIllustration() {
321
+ return (0, import_react.createElement)(
322
+ "svg",
323
+ { width: "115", height: "84", viewBox: "0 0 115 84", fill: "none", "aria-hidden": true },
324
+ (0, import_react.createElement)(
325
+ "g",
326
+ { clipPath: "url(#frDropClip)" },
327
+ (0, import_react.createElement)("rect", { y: "17.0742", width: "44.1832", height: "43.6431", rx: "12", transform: "rotate(-22.7338 0 17.0742)", fill: "#9CE5ED" }),
328
+ (0, import_react.createElement)("rect", { x: "73.4043", y: "8.54297", width: "43.7267", height: "50.5284", rx: "8", transform: "rotate(17.403 73.4043 8.54297)", fill: "#679EFE" }),
329
+ (0, import_react.createElement)("path", { d: "M30.4917 28.1369L40.8865 33.4564L37.2232 34.9524L29.5302 31.0159L26.7919 39.2122L23.1285 40.7082L26.8287 29.6338L16.8967 24.5516L20.5601 23.0556L27.7902 26.7549L30.3639 19.052L34.0273 17.556L30.4917 28.1369Z", fill: "white" }),
330
+ (0, import_react.createElement)("path", { d: "M77.5088 26.3047L101.057 33.7966", stroke: "white", strokeWidth: "3" }),
331
+ (0, import_react.createElement)("path", { d: "M72.2646 42.7871L86.3938 47.2823", stroke: "white", strokeWidth: "3" }),
332
+ (0, import_react.createElement)("path", { d: "M74.8867 34.5469L98.4353 42.0388", stroke: "white", strokeWidth: "3" }),
333
+ (0, import_react.createElement)("rect", { x: "31.583", y: "38.6641", width: "44.9157", height: "44.3666", rx: "12", transform: "rotate(-0.134233 31.583 38.6641)", fill: "#3964FE" }),
334
+ (0, import_react.createElement)("path", { d: "M38.9521 73.0337C39.6129 71.7086 41.7113 66.0937 43.5113 61.1663C44.1607 59.3885 46.7484 59.3923 47.4591 61.1465C48.9728 64.8828 50.7969 68.6922 51.9988 69.1925C54.2946 70.1482 57.9854 59.3573 68.0064 70.1801", stroke: "white", strokeWidth: "3" }),
335
+ (0, import_react.createElement)("circle", { cx: "60.6157", cy: "52.247", r: "4.38794", transform: "rotate(22.5996 60.6157 52.247)", fill: "white" })
336
+ ),
337
+ (0, import_react.createElement)("defs", null, (0, import_react.createElement)("clipPath", { id: "frDropClip" }, (0, import_react.createElement)("rect", { width: "115", height: "84", fill: "white" })))
338
+ );
339
+ }
340
+
341
+ // src/rail.tsx
342
+ function uid() {
343
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
344
+ }
345
+ async function uploadFile(sessionId, file) {
346
+ const id = uid();
347
+ const pending2 = {
348
+ id,
349
+ sessionId,
350
+ name: file.name,
351
+ relPath: "",
352
+ size: file.size,
353
+ mediaType: file.type || "application/octet-stream",
354
+ status: "uploading"
355
+ };
356
+ add(sessionId, pending2);
357
+ const url = `/plugins/file-native/upload?sessionId=${encodeURIComponent(sessionId)}&name=${encodeURIComponent(file.name)}&type=${encodeURIComponent(file.type)}`;
358
+ try {
359
+ const res = await fetch(url, { method: "POST", body: file });
360
+ const body = await res.json();
361
+ if (!res.ok || !body.ok || body.file === void 0) {
362
+ patch(sessionId, id, { status: "error", error: body.error ?? "\u4E0A\u4F20\u5931\u8D25" });
363
+ return { ...pending2, status: "error", error: body.error ?? "\u4E0A\u4F20\u5931\u8D25" };
364
+ }
365
+ patch(sessionId, id, {
366
+ status: "done",
367
+ name: body.file.name,
368
+ relPath: body.file.relPath,
369
+ size: body.file.size,
370
+ mediaType: body.file.mediaType
371
+ });
372
+ return { ...pending2, ...body.file, status: "done" };
373
+ } catch {
374
+ patch(sessionId, id, { status: "error", error: "\u7F51\u7EDC\u9519\u8BEF" });
375
+ return { ...pending2, status: "error", error: "\u7F51\u7EDC\u9519\u8BEF" };
376
+ }
377
+ }
378
+ function intake(sessionId, files, onAddImages) {
379
+ if (files.length === 0) return;
380
+ if (files.length > MAX_FILES_PER_BATCH) {
381
+ add(sessionId, {
382
+ id: uid(),
383
+ sessionId,
384
+ name: "\u6279\u6B21\u8FC7\u5927",
385
+ relPath: "",
386
+ size: 0,
387
+ mediaType: "",
388
+ status: "error",
389
+ error: `\u4E00\u6B21\u6700\u591A ${MAX_FILES_PER_BATCH} \u4E2A\u6587\u4EF6`
390
+ });
391
+ return;
392
+ }
393
+ const oversize = files.find((file) => file.size > MAX_FILE_BYTES);
394
+ if (oversize) {
395
+ add(sessionId, {
396
+ id: uid(),
397
+ sessionId,
398
+ name: oversize.name,
399
+ relPath: "",
400
+ size: oversize.size,
401
+ mediaType: oversize.type,
402
+ status: "error",
403
+ error: "\u8D85\u8FC7 50 MB"
404
+ });
405
+ return;
406
+ }
407
+ const { images, others } = splitIntake(files);
408
+ if (images.length > 0) onAddImages(images);
409
+ for (const file of others) void uploadFile(sessionId, file);
410
+ }
411
+ function Lightbox({ src, alt, onClose }) {
412
+ (0, import_react2.useEffect)(() => {
413
+ const onKey = (event) => {
414
+ if (event.key === "Escape") onClose();
415
+ };
416
+ window.addEventListener("keydown", onKey);
417
+ return () => window.removeEventListener("keydown", onKey);
418
+ }, [onClose]);
419
+ return (0, import_react2.createElement)(
420
+ "div",
421
+ { className: "fr-lightbox", role: "dialog", "aria-label": alt, onClick: onClose },
422
+ (0, import_react2.createElement)("img", { src, alt, onClick: (event) => event.stopPropagation() })
423
+ );
424
+ }
425
+ function ImageTile({ item, onRemove, onOpen }) {
426
+ return (0, import_react2.createElement)(
427
+ "div",
428
+ { className: "fr-item fr-item-image" },
429
+ (0, import_react2.createElement)(
430
+ "button",
431
+ { type: "button", className: "fr-thumb", title: item.file.name, onClick: onOpen },
432
+ (0, import_react2.createElement)("img", { src: item.previewUrl, alt: item.file.name })
433
+ ),
434
+ (0, import_react2.createElement)("button", { type: "button", className: "fr-remove", "aria-label": `\u79FB\u9664 ${item.file.name}`, onClick: onRemove }, (0, import_react2.createElement)(IconClose))
435
+ );
436
+ }
437
+ function FileTile({ item, sessionId }) {
438
+ const remove2 = () => {
439
+ remove(sessionId, item.id);
440
+ if (item.relPath) {
441
+ void fetch(`/plugins/file-native/remove?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(item.relPath)}`, { method: "POST" });
442
+ }
443
+ };
444
+ const meta = item.status === "uploading" ? "\u4E0A\u4F20\u4E2D\u2026" : item.status === "error" ? item.error ?? "\u5931\u8D25" : formatSize(item.size);
445
+ return (0, import_react2.createElement)(
446
+ "div",
447
+ { className: "fr-item", title: item.error ?? (item.relPath || item.name) },
448
+ (0, import_react2.createElement)(
449
+ "div",
450
+ { className: "fr-card", "data-status": item.status },
451
+ (0, import_react2.createElement)("span", { className: "fr-ext" }, extOf(item.name)),
452
+ (0, import_react2.createElement)(
453
+ "span",
454
+ { className: "fr-copy" },
455
+ (0, import_react2.createElement)("span", { className: "fr-name" }, item.name),
456
+ (0, import_react2.createElement)("span", { className: "fr-meta" }, meta)
457
+ )
458
+ ),
459
+ (0, import_react2.createElement)("button", { type: "button", className: "fr-remove", "aria-label": `\u79FB\u9664 ${item.name}`, onClick: remove2 }, (0, import_react2.createElement)(IconClose))
460
+ );
461
+ }
462
+ function FileRail(props) {
463
+ ensureStyles();
464
+ const sessionId = String(props.sessionId ?? "");
465
+ const [files, setFiles] = (0, import_react2.useState)(() => pending());
466
+ const [, setVersion] = (0, import_react2.useState)(() => version());
467
+ const [preview, setPreview] = (0, import_react2.useState)(null);
468
+ const [edges, setEdges] = (0, import_react2.useState)({ left: false, right: false });
469
+ const rowRef = (0, import_react2.useRef)(null);
470
+ const countRef = (0, import_react2.useRef)(0);
471
+ (0, import_react2.useEffect)(() => {
472
+ const sync = () => {
473
+ setFiles(pending());
474
+ setVersion(version());
475
+ };
476
+ sync();
477
+ return subscribe(sync);
478
+ }, []);
479
+ const updateEdges = () => {
480
+ const el = rowRef.current;
481
+ if (el === null) return;
482
+ const left = el.scrollLeft > 1;
483
+ const right = el.scrollLeft < el.scrollWidth - el.clientWidth - 1;
484
+ setEdges((prev) => prev.left === left && prev.right === right ? prev : { left, right });
485
+ };
486
+ const itemCount = props.attachments.length + files.length;
487
+ (0, import_react2.useLayoutEffect)(() => {
488
+ const grew = countRef.current !== 0 && itemCount > countRef.current;
489
+ countRef.current = itemCount;
490
+ const el = rowRef.current;
491
+ if (el === null) return;
492
+ if (grew) el.scrollLeft = el.scrollWidth - el.clientWidth;
493
+ updateEdges();
494
+ }, [itemCount]);
495
+ (0, import_react2.useEffect)(() => {
496
+ const el = rowRef.current;
497
+ if (el === null) return;
498
+ let disconnect = () => {
499
+ };
500
+ if (typeof ResizeObserver !== "undefined") {
501
+ const observer = new ResizeObserver(updateEdges);
502
+ observer.observe(el);
503
+ disconnect = () => observer.disconnect();
504
+ }
505
+ const onWheel = (event) => {
506
+ if (event.deltaY === 0) return;
507
+ event.preventDefault();
508
+ el.scrollBy({ left: Math.sign(event.deltaY) * Math.min(Math.abs(event.deltaY), 60), behavior: "auto" });
509
+ };
510
+ el.addEventListener("wheel", onWheel, { passive: false });
511
+ return () => {
512
+ disconnect();
513
+ el.removeEventListener("wheel", onWheel);
514
+ };
515
+ }, [itemCount]);
516
+ const page = (direction) => {
517
+ const el = rowRef.current;
518
+ if (el === null) return;
519
+ el.scrollBy({ left: direction * Math.max(el.clientWidth - 64, 200), behavior: "smooth" });
520
+ };
521
+ const hasImages = props.attachments.length > 0;
522
+ const hasFiles = files.length > 0;
523
+ if (!hasImages && !hasFiles) return null;
524
+ return (0, import_react2.createElement)(
525
+ "div",
526
+ { className: "fr-rail", "data-file-native": true },
527
+ (0, import_react2.createElement)(
528
+ "div",
529
+ { style: { position: "relative" } },
530
+ edges.left ? (0, import_react2.createElement)("button", { type: "button", className: "fr-arrow fr-arrow-left", "aria-label": "\u5411\u5DE6", onClick: () => page(-1) }, (0, import_react2.createElement)(IconChevronLeft)) : null,
531
+ (0, import_react2.createElement)(
532
+ "div",
533
+ { className: "fr-row", role: "group", "aria-label": "\u9644\u4EF6", ref: rowRef, onScroll: updateEdges },
534
+ ...props.attachments.map((item) => (0, import_react2.createElement)(ImageTile, {
535
+ key: item.id,
536
+ item,
537
+ onRemove: () => props.onRemoveImage(item.id),
538
+ onOpen: () => setPreview(item)
539
+ })),
540
+ ...files.map((item) => (0, import_react2.createElement)(FileTile, { key: item.id, item, sessionId }))
541
+ ),
542
+ edges.right ? (0, import_react2.createElement)("button", { type: "button", className: "fr-arrow fr-arrow-right", "aria-label": "\u5411\u53F3", onClick: () => page(1) }, (0, import_react2.createElement)(IconChevronRight)) : null
543
+ ),
544
+ preview ? (0, import_react2.createElement)(Lightbox, { src: preview.previewUrl, alt: preview.file.name, onClose: () => setPreview(null) }) : null
545
+ );
546
+ }
547
+ function intakeFiles(sessionId, files, onAddImages) {
548
+ intake(sessionId, files, onAddImages);
549
+ }
550
+
551
+ // src/picker.tsx
552
+ var import_react5 = require("react");
553
+
554
+ // src/overlay.tsx
555
+ var import_react3 = require("react");
556
+ var import_react_dom = require("react-dom");
557
+ function DropMask() {
558
+ const [target, setTarget] = (0, import_react3.useState)(null);
559
+ (0, import_react3.useEffect)(() => {
560
+ setTarget(document.body);
561
+ }, []);
562
+ const node = (0, import_react3.createElement)(
563
+ "div",
564
+ { className: "fr-mask", role: "status" },
565
+ (0, import_react3.createElement)(
566
+ "div",
567
+ { className: "fr-wrap" },
568
+ (0, import_react3.createElement)("div", { className: "fr-illust" }, (0, import_react3.createElement)(UploadIllustration)),
569
+ (0, import_react3.createElement)("div", { className: "fr-title" }, "\u677E\u5F00\u4EE5\u6DFB\u52A0\u6587\u4EF6"),
570
+ (0, import_react3.createElement)("div", { className: "fr-desc" }, "\u56FE\u7247\u8FDB\u5165\u8349\u7A3F\u56FE\u680F\uFF0C\u5176\u5B83\u6587\u4EF6\u4FDD\u5B58\u5230\u5DE5\u4F5C\u533A")
571
+ )
572
+ );
573
+ return target ? (0, import_react_dom.createPortal)(node, target) : node;
574
+ }
575
+
576
+ // src/rail-portal.tsx
577
+ var import_react4 = require("react");
578
+ var import_react_dom2 = require("react-dom");
579
+
580
+ // src/live.ts
581
+ var owner = null;
582
+ var lastSessionId = "";
583
+ var dragDepth = 0;
584
+ var listeners2 = /* @__PURE__ */ new Set();
585
+ function emit2() {
586
+ for (const fn of [...listeners2]) fn();
587
+ }
588
+ function subscribeLive(fn) {
589
+ listeners2.add(fn);
590
+ return () => {
591
+ listeners2.delete(fn);
592
+ };
593
+ }
594
+ function setLiveOwner(next) {
595
+ owner = next;
596
+ if (next?.sessionId) lastSessionId = next.sessionId;
597
+ emit2();
598
+ }
599
+ function getLiveOwner() {
600
+ return owner;
601
+ }
602
+ function setDragDepth(depth) {
603
+ const next = Math.max(0, depth);
604
+ if (next === dragDepth) return;
605
+ dragDepth = next;
606
+ emit2();
607
+ }
608
+ function getDragDepth() {
609
+ return dragDepth;
610
+ }
611
+ function rememberSessionId(id) {
612
+ if (id) lastSessionId = id;
613
+ }
614
+ function currentSessionId() {
615
+ return owner?.sessionId || lastSessionId;
616
+ }
617
+ function resolveSessionId(props) {
618
+ if (props.sessionId) return String(props.sessionId);
619
+ if (props.session?.id) return String(props.session.id);
620
+ return currentSessionId();
621
+ }
622
+
623
+ // src/rail-portal.tsx
624
+ function placeHost(anchor) {
625
+ const card = anchor?.closest("[data-composer-card]");
626
+ if (card === null) return null;
627
+ let el = card.querySelector("[data-file-native-host]");
628
+ if (el === null) {
629
+ el = document.createElement("div");
630
+ el.dataset.fileNativeHost = "";
631
+ }
632
+ const scroll = card.querySelector("[data-input-scroll]");
633
+ if (el.parentElement !== card || scroll !== null && el.nextElementSibling !== scroll) {
634
+ if (scroll !== null) card.insertBefore(el, scroll);
635
+ else if (el.parentElement !== card) card.insertBefore(el, card.firstChild);
636
+ }
637
+ return el;
638
+ }
639
+ function ComposerRail(props) {
640
+ ensureStyles();
641
+ const anchorRef = (0, import_react4.useRef)(null);
642
+ const [host, setHost] = (0, import_react4.useState)(null);
643
+ const [, bump] = (0, import_react4.useState)(0);
644
+ (0, import_react4.useEffect)(() => {
645
+ const a = subscribe(() => bump((n) => n + 1));
646
+ const b = subscribeLive(() => bump((n) => n + 1));
647
+ return () => {
648
+ a();
649
+ b();
650
+ };
651
+ }, []);
652
+ (0, import_react4.useEffect)(() => {
653
+ let cancelled = false;
654
+ let tries = 0;
655
+ const tick = () => {
656
+ if (cancelled) return;
657
+ const el = placeHost(anchorRef.current);
658
+ if (el !== null) {
659
+ setHost((prev) => prev === el ? prev : el);
660
+ return;
661
+ }
662
+ tries += 1;
663
+ if (tries < 30) window.setTimeout(tick, 50);
664
+ };
665
+ tick();
666
+ return () => {
667
+ cancelled = true;
668
+ };
669
+ }, []);
670
+ const live = getLiveOwner();
671
+ const rail = (0, import_react4.createElement)(FileRail, {
672
+ attachments: live?.attachments ?? [],
673
+ canAcceptDrop: live?.canAcceptDrop ?? true,
674
+ onAddImages: live?.onAddImages ?? (() => {
675
+ }),
676
+ onRemoveImage: live?.onRemoveImage ?? (() => {
677
+ }),
678
+ sessionId: live?.sessionId || props.sessionId || currentSessionId(),
679
+ dropLimits: live?.dropLimits
680
+ });
681
+ return (0, import_react4.createElement)("span", {
682
+ ref: anchorRef,
683
+ "aria-hidden": true,
684
+ style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }
685
+ }, host !== null && host.isConnected ? (0, import_react_dom2.createPortal)(rail, host) : null);
686
+ }
687
+
688
+ // src/picker.tsx
689
+ function PaperclipButton(props) {
690
+ ensureStyles();
691
+ const inputRef = (0, import_react5.useRef)(null);
692
+ const [drag, setDrag] = (0, import_react5.useState)(() => getDragDepth() > 0);
693
+ const sessionId = String(props.sessionId || currentSessionId());
694
+ rememberSessionId(sessionId);
695
+ (0, import_react5.useEffect)(() => subscribeLive(() => setDrag(getDragDepth() > 0)), []);
696
+ const onChange = (event) => {
697
+ const files = Array.from(event.currentTarget.files ?? []);
698
+ event.currentTarget.value = "";
699
+ const live = getLiveOwner();
700
+ const sid = String(props.sessionId || live?.sessionId || currentSessionId());
701
+ if (files.length === 0) return;
702
+ intakeFiles(sid, files, live?.onAddImages ?? (() => {
703
+ }));
704
+ };
705
+ return (0, import_react5.createElement)(
706
+ "span",
707
+ null,
708
+ (0, import_react5.createElement)(ComposerRail, { sessionId }),
709
+ drag ? (0, import_react5.createElement)(DropMask) : null,
710
+ (0, import_react5.createElement)("button", {
711
+ type: "button",
712
+ className: "fr-pick",
713
+ title: "\u4E0A\u4F20\u6587\u4EF6",
714
+ "aria-label": "\u4E0A\u4F20\u6587\u4EF6",
715
+ onClick: () => inputRef.current?.click()
716
+ }, (0, import_react5.createElement)(IconPaperclip, { size: 16 })),
717
+ (0, import_react5.createElement)("input", {
718
+ ref: inputRef,
719
+ className: "fr-hidden",
720
+ type: "file",
721
+ multiple: true,
722
+ onChange
723
+ })
724
+ );
725
+ }
726
+
727
+ // src/tail.tsx
728
+ var import_react6 = require("react");
729
+ function basename(path) {
730
+ const at = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
731
+ return at === -1 ? path : path.slice(at + 1);
732
+ }
733
+ function ChipRow({ label, items: items2, open }) {
734
+ if (items2.length === 0) return null;
735
+ const shown = items2.slice(0, 6);
736
+ const hidden = items2.length - shown.length;
737
+ return (0, import_react6.createElement)(
738
+ "div",
739
+ { className: "fr-tail" },
740
+ (0, import_react6.createElement)("span", { className: "fr-tail-label" }, label),
741
+ (0, import_react6.createElement)(
742
+ "div",
743
+ { className: "fr-tail-row" },
744
+ ...shown.map((item) => (0, import_react6.createElement)("button", {
745
+ key: item.path,
746
+ type: "button",
747
+ className: "fr-chip",
748
+ title: item.path,
749
+ onClick: () => open(item.path)
750
+ }, item.name)),
751
+ hidden > 0 ? (0, import_react6.createElement)("span", { className: "fr-more" }, `+ ${hidden} \u4E2A\u6587\u4EF6`) : null
752
+ )
753
+ );
754
+ }
755
+ function UploadedTail(props) {
756
+ ensureStyles();
757
+ const sessionId = String(props.sessionId ?? "");
758
+ const [uploaded, setUploaded] = (0, import_react6.useState)(
759
+ props.matched?.uploaded ?? (props.turn !== void 0 ? filesForTurn(sessionId, props.turn) : lastSent(sessionId))
760
+ );
761
+ (0, import_react6.useEffect)(() => {
762
+ if (props.matched?.uploaded) {
763
+ setUploaded(props.matched.uploaded);
764
+ return;
765
+ }
766
+ let cancelled = false;
767
+ void fetch(`/plugins/file-native/last?sessionId=${encodeURIComponent(sessionId)}`).then((res) => res.json()).then((body) => {
768
+ if (!cancelled && Array.isArray(body.files) && body.files.length > 0) setUploaded(body.files);
769
+ }).catch(() => {
770
+ });
771
+ return () => {
772
+ cancelled = true;
773
+ };
774
+ }, [sessionId, props.matched, props.turn]);
775
+ const produced = props.matched?.produced ?? [];
776
+ if (uploaded.length === 0 && produced.length === 0) return null;
777
+ const open = props.openFile ?? ((path) => {
778
+ window.open(path, "_blank");
779
+ });
780
+ return (0, import_react6.createElement)(
781
+ "div",
782
+ { "data-file-native-tail": true },
783
+ (0, import_react6.createElement)(ChipRow, {
784
+ label: "\u9644\u4EF6",
785
+ items: uploaded.map((file) => ({ name: file.name, path: file.relPath })),
786
+ open
787
+ }),
788
+ (0, import_react6.createElement)(ChipRow, {
789
+ label: "\u4EA7\u7269",
790
+ items: produced.map((path) => ({ name: basename(path), path })),
791
+ open
792
+ })
793
+ );
794
+ }
795
+ function producedPathsOf(owner2) {
796
+ const data = owner2.turn?.data?.get?.("deliverables");
797
+ const seq = owner2.seq ?? Number.POSITIVE_INFINITY;
798
+ if (!data?.produced) return [];
799
+ const paths = [];
800
+ const seen = /* @__PURE__ */ new Set();
801
+ for (const item of data.produced) {
802
+ if (item.seq > seq || seen.has(item.path)) continue;
803
+ seen.add(item.path);
804
+ paths.push(item.path);
805
+ }
806
+ return paths;
807
+ }
808
+
809
+ // src/context.tsx
810
+ var import_react7 = require("react");
811
+ var import_react_dom3 = require("react-dom");
812
+ function FileCard({ file, onOpen }) {
813
+ return (0, import_react7.createElement)(
814
+ "button",
815
+ {
816
+ type: "button",
817
+ className: "fr-msg-card",
818
+ title: file.relPath,
819
+ onClick: () => onOpen?.(file.relPath)
820
+ },
821
+ (0, import_react7.createElement)("span", { className: "fr-ext" }, extOf(file.name)),
822
+ (0, import_react7.createElement)(
823
+ "span",
824
+ { className: "fr-copy" },
825
+ (0, import_react7.createElement)("span", { className: "fr-name" }, file.name),
826
+ (0, import_react7.createElement)("span", { className: "fr-meta" }, formatSize(file.size))
827
+ )
828
+ );
829
+ }
830
+ function userStackOf(el) {
831
+ if (!(el instanceof HTMLElement)) return null;
832
+ const kind = el.getAttribute("data-chat-flow-kind");
833
+ if (kind !== "user" && kind !== "steering") return null;
834
+ const row = el.querySelector("[data-time-hover-root]");
835
+ const stack = row?.firstElementChild;
836
+ return stack instanceof HTMLElement ? stack : null;
837
+ }
838
+ function findUserStack(anchor) {
839
+ const flow = anchor?.closest('[data-chat-flow-kind="context"]');
840
+ if (flow === null) return null;
841
+ let next = flow.nextElementSibling;
842
+ let prev = flow.previousElementSibling;
843
+ for (let hop = 0; hop < 16; hop += 1) {
844
+ const stack = userStackOf(next) ?? userStackOf(prev);
845
+ if (stack !== null) return stack;
846
+ next = next?.nextElementSibling ?? null;
847
+ prev = prev?.previousElementSibling ?? null;
848
+ }
849
+ return null;
850
+ }
851
+ function alreadyPlaced(el, stack, before) {
852
+ if (el.parentElement !== stack) return false;
853
+ if (before === el) return true;
854
+ return el.nextSibling === before;
855
+ }
856
+ function placeHost2(stack) {
857
+ let el = stack.querySelector("[data-file-native-msg]");
858
+ if (el === null) {
859
+ el = document.createElement("div");
860
+ el.dataset.fileNativeMsg = "";
861
+ el.className = "fr-msg-row";
862
+ }
863
+ const gallery = stack.querySelector("[data-align]");
864
+ const after = gallery instanceof HTMLElement ? gallery.closest('[data-slot="conversation.message.images"]') ?? gallery : null;
865
+ const before = after?.nextSibling ?? stack.firstChild;
866
+ if (!alreadyPlaced(el, stack, before)) {
867
+ stack.insertBefore(el, before);
868
+ }
869
+ return el;
870
+ }
871
+ function AttachToUserBubble(props) {
872
+ const anchorRef = (0, import_react7.useRef)(null);
873
+ const [host, setHost] = (0, import_react7.useState)(null);
874
+ const noticeKey = props.files.map((file) => file.relPath).join("\n");
875
+ (0, import_react7.useEffect)(() => {
876
+ const sid = currentSessionId();
877
+ if (sid) archiveIfNoticeMatches(sid, props.files);
878
+ }, [noticeKey]);
879
+ (0, import_react7.useEffect)(() => {
880
+ let cancelled = false;
881
+ let tries = 0;
882
+ const tick = () => {
883
+ if (cancelled) return;
884
+ const stack = findUserStack(anchorRef.current);
885
+ if (stack !== null) {
886
+ const el = placeHost2(stack);
887
+ setHost((prev) => prev === el ? prev : el);
888
+ return;
889
+ }
890
+ tries += 1;
891
+ if (tries < 30) window.setTimeout(tick, 50);
892
+ };
893
+ tick();
894
+ return () => {
895
+ cancelled = true;
896
+ };
897
+ }, []);
898
+ const gallery = (0, import_react7.createElement)(
899
+ "div",
900
+ { className: "fr-msg-row-inner" },
901
+ ...props.files.map((file) => (0, import_react7.createElement)(FileCard, { key: file.relPath, file, onOpen: props.openFile }))
902
+ );
903
+ return (0, import_react7.createElement)("span", {
904
+ ref: anchorRef,
905
+ "data-file-native-anchor": true,
906
+ "aria-hidden": true
907
+ }, host !== null && host.isConnected ? (0, import_react_dom3.createPortal)(gallery, host) : null);
908
+ }
909
+ function OtherContext({ node }) {
910
+ const [open, setOpen] = (0, import_react7.useState)(false);
911
+ const data = node.data ?? {};
912
+ const text = contentText(data.content);
913
+ const summary = data.source?.summary || data.provenance?.label || "\u4E0A\u4E0B\u6587\u6CE8\u5165";
914
+ const source = data.source?.plugin || data.provenance?.label || "";
915
+ return (0, import_react7.createElement)(
916
+ "div",
917
+ { className: "fr-ctx", "data-open": open || void 0 },
918
+ (0, import_react7.createElement)(
919
+ "button",
920
+ {
921
+ type: "button",
922
+ className: "fr-ctx-head",
923
+ onClick: () => setOpen((value) => !value)
924
+ },
925
+ (0, import_react7.createElement)("span", { className: "fr-ctx-title" }, "\u4E0A\u4E0B\u6587\u6CE8\u5165"),
926
+ source ? (0, import_react7.createElement)("span", { className: "fr-ctx-src" }, source) : null,
927
+ (0, import_react7.createElement)("span", { className: "fr-ctx-sum" }, summary)
928
+ ),
929
+ open ? (0, import_react7.createElement)("pre", { className: "fr-ctx-body" }, text) : null
930
+ );
931
+ }
932
+ function ContextNodeView(props) {
933
+ ensureStyles();
934
+ const node = props.node ?? {};
935
+ const source = node.data?.source;
936
+ if (source?.kind === "plugin" && source.plugin === "file-native") {
937
+ const files = parseNoticeFiles(contentText(node.data?.content));
938
+ if (files.length === 0) return null;
939
+ return (0, import_react7.createElement)(AttachToUserBubble, { files, openFile: props.openFile });
940
+ }
941
+ return (0, import_react7.createElement)(OtherContext, { node });
942
+ }
943
+
944
+ // src/intercept.ts
945
+ function looksLikeFileDrag(transfer) {
946
+ if (transfer === null) return false;
947
+ const types = Array.from(transfer.types);
948
+ return types.length === 0 || types.includes("Files");
949
+ }
950
+ function installFileIntercept(handlers) {
951
+ const state = { depth: 0, aborted: false };
952
+ const reset = () => {
953
+ state.depth = 0;
954
+ state.aborted = false;
955
+ };
956
+ const onDragEnter = (event) => {
957
+ if (!looksLikeFileDrag(event.dataTransfer) || !handlers.canAccept()) return;
958
+ event.preventDefault();
959
+ event.stopPropagation();
960
+ state.aborted = false;
961
+ state.depth += 1;
962
+ handlers.onDepth?.(state.depth);
963
+ };
964
+ const onDragOver = (event) => {
965
+ if (!looksLikeFileDrag(event.dataTransfer) || !handlers.canAccept()) return;
966
+ event.preventDefault();
967
+ event.stopPropagation();
968
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
969
+ };
970
+ const onDragLeave = (event) => {
971
+ if (!looksLikeFileDrag(event.dataTransfer)) return;
972
+ event.preventDefault();
973
+ event.stopPropagation();
974
+ state.depth = Math.max(0, state.depth - 1);
975
+ const leavingViewport = event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight;
976
+ if (state.depth === 0 || leavingViewport) {
977
+ reset();
978
+ handlers.onDepth?.(0);
979
+ return;
980
+ }
981
+ handlers.onDepth?.(state.depth);
982
+ };
983
+ const onDrop = (event) => {
984
+ if (!looksLikeFileDrag(event.dataTransfer) && (event.dataTransfer?.files.length ?? 0) === 0) return;
985
+ event.preventDefault();
986
+ event.stopPropagation();
987
+ const aborted = state.aborted;
988
+ const files = Array.from(event.dataTransfer?.files ?? []);
989
+ reset();
990
+ handlers.onDepth?.(0);
991
+ try {
992
+ window.dispatchEvent(new DragEvent("dragend"));
993
+ } catch {
994
+ }
995
+ if (aborted || !handlers.canAccept() || files.length === 0) return;
996
+ handlers.onFiles(files);
997
+ };
998
+ const onKeyDown = (event) => {
999
+ if (event.key !== "Escape" || state.depth === 0) return;
1000
+ event.preventDefault();
1001
+ event.stopPropagation();
1002
+ state.aborted = true;
1003
+ reset();
1004
+ handlers.onDepth?.(0);
1005
+ };
1006
+ const onPaste = (event) => {
1007
+ if (!handlers.canAccept()) return;
1008
+ const fromList = Array.from(event.clipboardData?.files ?? []);
1009
+ const fromItems = Array.from(event.clipboardData?.items ?? []).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((file) => file !== null);
1010
+ const files = fromList.length > 0 ? fromList : fromItems;
1011
+ if (files.length === 0) return;
1012
+ event.preventDefault();
1013
+ event.stopPropagation();
1014
+ handlers.onFiles(files);
1015
+ };
1016
+ document.addEventListener("dragenter", onDragEnter, true);
1017
+ document.addEventListener("dragover", onDragOver, true);
1018
+ document.addEventListener("dragleave", onDragLeave, true);
1019
+ document.addEventListener("drop", onDrop, true);
1020
+ document.addEventListener("paste", onPaste, true);
1021
+ window.addEventListener("keydown", onKeyDown, true);
1022
+ return () => {
1023
+ document.removeEventListener("dragenter", onDragEnter, true);
1024
+ document.removeEventListener("dragover", onDragOver, true);
1025
+ document.removeEventListener("dragleave", onDragLeave, true);
1026
+ document.removeEventListener("drop", onDrop, true);
1027
+ document.removeEventListener("paste", onPaste, true);
1028
+ window.removeEventListener("keydown", onKeyDown, true);
1029
+ };
1030
+ }
1031
+
1032
+ // src/client.ts
1033
+ var name = "file-native";
1034
+ var inject = ["slots"];
1035
+ function RailSlot(props) {
1036
+ ensureStyles();
1037
+ const sessionId = resolveSessionId(props);
1038
+ setLiveOwner({
1039
+ attachments: props.attachments,
1040
+ canAcceptDrop: props.canAcceptDrop,
1041
+ onAddImages: props.onAddImages,
1042
+ onRemoveImage: props.onRemoveImage,
1043
+ sessionId,
1044
+ dropLimits: props.dropLimits
1045
+ });
1046
+ (0, import_react8.useEffect)(() => () => setLiveOwner(null), []);
1047
+ return null;
1048
+ }
1049
+ function PickerSlot(props) {
1050
+ const sessionId = resolveSessionId(props);
1051
+ rememberSessionId(sessionId);
1052
+ const phase = String(props.input?.phase ?? "");
1053
+ const draft = String(props.input?.draft ?? "");
1054
+ const imageCount = props.input?.imageIds?.length ?? 0;
1055
+ const prev = (0, import_react8.useRef)({ phase, draft, imageCount });
1056
+ (0, import_react8.useEffect)(() => {
1057
+ const sid = sessionId || currentSessionId();
1058
+ const was = prev.current;
1059
+ const entering = phase === "submitting" || phase === "claimed";
1060
+ const leaving = (was.phase === "submitting" || was.phase === "claimed") && phase === "plain";
1061
+ const sentPlain = was.phase !== "plain" && phase === "plain" && draft.trim() === "" && imageCount === 0;
1062
+ if (sid && (entering || leaving || sentPlain)) archiveSent(sid);
1063
+ prev.current = { phase, draft, imageCount };
1064
+ }, [phase, draft, imageCount, sessionId]);
1065
+ return (0, import_react8.createElement)(PaperclipButton, { sessionId });
1066
+ }
1067
+ function TailSlot(props) {
1068
+ return (0, import_react8.createElement)(UploadedTail, {
1069
+ sessionId: props.sessionId,
1070
+ openFile: props.openFile,
1071
+ matched: props.matched ?? null,
1072
+ turn: props.turn?.turn
1073
+ });
1074
+ }
1075
+ function selectTail(owner2) {
1076
+ const produced = producedPathsOf(owner2);
1077
+ if (produced.length === 0) return null;
1078
+ return { uploaded: [], produced };
1079
+ }
1080
+ function installLiveIntercept() {
1081
+ return installFileIntercept({
1082
+ canAccept: () => true,
1083
+ onDepth: (depth) => setDragDepth(depth),
1084
+ onFiles: (files) => {
1085
+ const live = getLiveOwner();
1086
+ const sessionId = live?.sessionId || currentSessionId();
1087
+ const addImages = live?.onAddImages ?? (() => {
1088
+ });
1089
+ intakeFiles(sessionId, files, addImages);
1090
+ }
1091
+ });
1092
+ }
1093
+ function apply(ctx) {
1094
+ ctx.effect(() => installLiveIntercept(), "file-native: document intercept");
1095
+ ctx.slots.inject("conversation.input.attachments", () => ctx.slots.register({
1096
+ name: "conversation.input.attachments",
1097
+ priority: -1
1098
+ }, RailSlot));
1099
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
1100
+ name: "conversation.input.left",
1101
+ id: "file-native-picker",
1102
+ order: 0
1103
+ }, PickerSlot));
1104
+ ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
1105
+ name: "conversation.chat.node",
1106
+ key: "context",
1107
+ priority: -1
1108
+ }, ContextNodeView));
1109
+ ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
1110
+ name: "conversation.chat.turnTail",
1111
+ priority: -1,
1112
+ select: selectTail
1113
+ }, TailSlot));
1114
+ ctx.logger?.info?.("[file-native] client loaded");
1115
+ }
1116
+
1117
+ return module.exports;
1118
+ }});