dsh-workbuddy-files 0.1.0 → 0.1.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.
Files changed (2) hide show
  1. package/lib/client.js +892 -891
  2. package/package.json +3 -3
package/lib/client.js CHANGED
@@ -1,645 +1,598 @@
1
- //#region src/client/lib/icons.ts
2
- /** 文件类型 → 图标/颜色映射(WorkBuddy 风格的类型标识) */
3
- const ICONS = {
4
- pdf: "📕",
5
- doc: "📘",
6
- docx: "📘",
7
- xls: "📊",
8
- xlsx: "📊",
9
- csv: "📊",
10
- ppt: "📙",
11
- pptx: "📙",
12
- png: "🖼️",
13
- jpg: "🖼️",
14
- jpeg: "🖼️",
15
- gif: "🖼️",
16
- webp: "🖼️",
17
- svg: "🖼️",
18
- bmp: "🖼️",
19
- zip: "🗜️",
20
- "7z": "🗜️",
21
- rar: "🗜️",
22
- tar: "🗜️",
23
- gz: "🗜️",
24
- mp3: "🎵",
25
- wav: "🎵",
26
- flac: "🎵",
27
- mp4: "🎬",
28
- mov: "🎬",
29
- mkv: "🎬",
30
- md: "📝",
31
- txt: "📄",
32
- log: "📄",
33
- js: "💻",
34
- ts: "💻",
35
- jsx: "💻",
36
- tsx: "💻",
37
- py: "💻",
38
- go: "💻",
39
- rs: "💻",
40
- java: "💻",
41
- c: "💻",
42
- h: "💻",
43
- cpp: "💻",
44
- cs: "💻",
45
- json: "💻",
46
- yaml: "💻",
47
- yml: "💻",
48
- toml: "💻",
49
- sh: "💻",
50
- ps1: "💻",
51
- bat: "💻",
52
- css: "💻",
53
- html: "💻",
54
- vue: "💻",
55
- sql: "💻"
56
- };
57
- function iconFor(name) {
58
- const dot = String(name).lastIndexOf(".");
59
- const ext = dot >= 0 ? String(name).slice(dot + 1).toLowerCase() : "";
60
- return ICONS[ext] ?? "📄";
61
- }
62
- function formatSize(n) {
63
- if (n === null || n === void 0 || !Number.isFinite(n)) return "";
64
- if (n < 1024) return n + " B";
65
- if (n < 1048576) return (n / 1024).toFixed(1) + " KB";
66
- if (n < 1073741824) return (n / 1048576).toFixed(1) + " MB";
67
- return (n / 1073741824).toFixed(2) + " GB";
68
- }
69
- //#endregion
70
- //#region src/client/lib/transfer.ts
71
- /** 引用文本格式:无空格直接 @path;含空格/引号用 @"path";目录保留尾斜杠 */
72
- function mentionFor(path, isDir) {
73
- let p = String(path);
74
- if (isDir) p = p.replace(/[\\/]+$/, "") + "/";
75
- if (/\s|"/.test(p)) return "@\"" + p + "\"";
76
- return "@" + p;
77
- }
78
- /** 从用户消息文本中提取 @ 文件引用(消息发送后的序列化形式) */
79
- function extractRefs(content) {
80
- const out = [];
81
- for (const b of content) if (b !== null && b !== void 0 && b.type === "text" && typeof b.text === "string") {
82
- const re = /@"([^"]+)"|@([^\s"@]+)/g;
83
- let m;
84
- while ((m = re.exec(b.text)) !== null) {
85
- const p = (m[1] !== void 0 ? m[1] : m[2]).trim();
86
- if (p !== "" && !out.includes(p)) out.push(p);
87
- }
1
+ (function() {
2
+ //#region src/client/lib/icons.ts
3
+ /** 文件类型 → 图标/颜色映射(WorkBuddy 风格的类型标识) */
4
+ const ICONS = {
5
+ pdf: "📕",
6
+ doc: "📘",
7
+ docx: "📘",
8
+ xls: "📊",
9
+ xlsx: "📊",
10
+ csv: "📊",
11
+ ppt: "📙",
12
+ pptx: "📙",
13
+ png: "🖼️",
14
+ jpg: "🖼️",
15
+ jpeg: "🖼️",
16
+ gif: "🖼️",
17
+ webp: "🖼️",
18
+ svg: "🖼️",
19
+ bmp: "🖼️",
20
+ zip: "🗜️",
21
+ "7z": "🗜️",
22
+ rar: "🗜️",
23
+ tar: "🗜️",
24
+ gz: "🗜️",
25
+ mp3: "🎵",
26
+ wav: "🎵",
27
+ flac: "🎵",
28
+ mp4: "🎬",
29
+ mov: "🎬",
30
+ mkv: "🎬",
31
+ md: "📝",
32
+ txt: "📄",
33
+ log: "📄",
34
+ js: "💻",
35
+ ts: "💻",
36
+ jsx: "💻",
37
+ tsx: "💻",
38
+ py: "💻",
39
+ go: "💻",
40
+ rs: "💻",
41
+ java: "💻",
42
+ c: "💻",
43
+ h: "💻",
44
+ cpp: "💻",
45
+ cs: "💻",
46
+ json: "💻",
47
+ yaml: "💻",
48
+ yml: "💻",
49
+ toml: "💻",
50
+ sh: "💻",
51
+ ps1: "💻",
52
+ bat: "💻",
53
+ css: "💻",
54
+ html: "💻",
55
+ vue: "💻",
56
+ sql: "💻"
57
+ };
58
+ function iconFor(name) {
59
+ const dot = String(name).lastIndexOf(".");
60
+ const ext = dot >= 0 ? String(name).slice(dot + 1).toLowerCase() : "";
61
+ return ICONS[ext] ?? "📄";
88
62
  }
89
- return out.slice(0, 40);
90
- }
91
- /**
92
- * 后台上传任务队列:逐个落盘(目录任务先遍历),不阻塞输入。
93
- * 交付物版本走 webServer 二进制路由(/workbuddy-drops),无 base64、无 JSON 体积上限。
94
- * @returns 成功数量与失败清单(文件名 + 原因)
95
- */
96
- async function runUploadJobs(jobs, batch, maxFileBytes = 268435456) {
97
- let ok = 0;
98
- const failed = [];
99
- for (const j of jobs) {
100
- const files = j.kind === "dir" ? await walkEntry(j.entry, "") : [{
101
- rel: j.rel,
102
- name: j.name,
103
- size: j.file.size,
104
- file: j.file
105
- }];
106
- for (const f of files) {
107
- if (f.file.size > maxFileBytes) {
108
- failed.push(f.name + "(超过大小上限)");
109
- continue;
110
- }
111
- const url = "/workbuddy-drops/save?batch=" + encodeURIComponent(batch) + "&rel=" + encodeURIComponent(f.rel);
112
- const res = await fetch(url, {
113
- method: "POST",
114
- body: f.file
115
- });
116
- if (res.ok) {
117
- const body = await res.json();
118
- if (body.ok === true) {
119
- ok += 1;
120
- continue;
121
- }
122
- failed.push(f.name + "(" + (body.error ?? "写入失败") + ")");
123
- } else failed.push(f.name + "(HTTP " + res.status + ")");
124
- }
63
+ function formatSize(n) {
64
+ if (n === null || n === void 0 || !Number.isFinite(n)) return "";
65
+ if (n < 1024) return n + " B";
66
+ if (n < 1048576) return (n / 1024).toFixed(1) + " KB";
67
+ if (n < 1073741824) return (n / 1048576).toFixed(1) + " MB";
68
+ return (n / 1073741824).toFixed(2) + " GB";
125
69
  }
126
- return {
127
- ok,
128
- failed
129
- };
130
- }
131
- /** 缓存根目录(~/.dsh-drops) */
132
- async function dropsHome() {
133
- try {
134
- const body = await (await fetch("/workbuddy-drops/home")).json();
135
- if (body.ok === true && typeof body.root === "string") return body.root;
136
- return null;
137
- } catch {
138
- return null;
70
+ //#endregion
71
+ //#region src/client/lib/transfer.ts
72
+ /** 引用文本格式:无空格直接 @path;含空格/引号用 @"path";目录保留尾斜杠 */
73
+ function mentionFor(path, isDir) {
74
+ let p = String(path);
75
+ if (isDir) p = p.replace(/[\\/]+$/, "") + "/";
76
+ if (/\s|"/.test(p)) return "@\"" + p + "\"";
77
+ return "@" + p;
139
78
  }
140
- }
141
- function walkEntry(entry, rel) {
142
- return new Promise((resolve2) => {
143
- if (entry.isFile) {
144
- entry.file((f) => resolve2([{
145
- rel: rel === "" ? entry.name : rel + "/" + entry.name,
146
- name: entry.name,
147
- size: f.size,
148
- file: f
149
- }]), () => resolve2([]));
150
- return;
79
+ /** 从用户消息文本中提取 @ 文件引用(消息发送后的序列化形式) */
80
+ function extractRefs(content) {
81
+ const out = [];
82
+ for (const b of content) if (b !== null && b !== void 0 && b.type === "text" && typeof b.text === "string") {
83
+ const re = /@"([^"]+)"|@([^\s"@]+)/g;
84
+ let m;
85
+ while ((m = re.exec(b.text)) !== null) {
86
+ const p = (m[1] !== void 0 ? m[1] : m[2]).trim();
87
+ if (p !== "" && !out.includes(p)) out.push(p);
88
+ }
151
89
  }
152
- if (entry.isDirectory) {
153
- const reader = entry.createReader();
154
- const found = [];
155
- const readBatch = () => reader.readEntries((ents) => {
156
- if (ents.length === 0) {
157
- Promise.all(found.map((e) => walkEntry(e, rel === "" ? entry.name : rel + "/" + entry.name))).then((rs) => resolve2(rs.flat())).catch(() => resolve2([]));
158
- return;
90
+ return out.slice(0, 40);
91
+ }
92
+ /**
93
+ * 后台上传任务队列:逐个落盘(目录任务先遍历),不阻塞输入。
94
+ * 交付物版本走 webServer 二进制路由(/workbuddy-drops),无 base64、无 JSON 体积上限。
95
+ * @returns 成功数量与失败清单(文件名 + 原因)
96
+ */
97
+ async function runUploadJobs(jobs, batch, maxFileBytes = 268435456) {
98
+ let ok = 0;
99
+ const failed = [];
100
+ for (const j of jobs) {
101
+ const files = j.kind === "dir" ? await walkEntry(j.entry, "") : [{
102
+ rel: j.rel,
103
+ name: j.name,
104
+ size: j.file.size,
105
+ file: j.file
106
+ }];
107
+ for (const f of files) {
108
+ if (f.file.size > maxFileBytes) {
109
+ failed.push(f.name + "(超过大小上限)");
110
+ continue;
159
111
  }
160
- found.push(...ents);
161
- readBatch();
162
- }, () => resolve2([]));
163
- readBatch();
164
- return;
112
+ const url = "/workbuddy-drops/save?batch=" + encodeURIComponent(batch) + "&rel=" + encodeURIComponent(f.rel);
113
+ const res = await fetch(url, {
114
+ method: "POST",
115
+ body: f.file
116
+ });
117
+ if (res.ok) {
118
+ const body = await res.json();
119
+ if (body.ok === true) {
120
+ ok += 1;
121
+ continue;
122
+ }
123
+ failed.push(f.name + "(" + (body.error ?? "写入失败") + ")");
124
+ } else failed.push(f.name + "(HTTP " + res.status + ")");
125
+ }
165
126
  }
166
- resolve2([]);
167
- });
168
- }
169
- async function dropsList(query) {
170
- try {
171
- return (await (await fetch("/workbuddy-drops/list?query=" + encodeURIComponent(query))).json()).items ?? [];
172
- } catch {
173
- return [];
174
- }
175
- }
176
- async function dropsStat(path) {
177
- try {
178
- return await (await fetch("/workbuddy-drops/stat?path=" + encodeURIComponent(path))).json();
179
- } catch {
180
127
  return {
181
- ok: false,
182
- path
128
+ ok,
129
+ failed
183
130
  };
184
131
  }
185
- }
186
- //#endregion
187
- //#region src/client/at-source.ts
188
- /**
189
- * @ 触发源「workbuddy」:输入 @ 时在菜单中追加「文件缓存」分组 ——
190
- * ~/.dsh-drops 中拖拽/粘贴/选择落地的文件与目录树,支持搜索。
191
- * 选中后由输入管线的 onPick → { insert } 在触发词位置铸造原生气泡。
192
- *
193
- * 工作区文件/文件夹的 @ 检索由 DSH 官方 ui-reference 源提供(文件与文件夹 +
194
- * Session 分组),本插件与之共存,无需重复实现。
195
- *
196
- * 所有引用在拖入时已完成落地(真实绝对路径),因此 codec 序列化是恒等函数,
197
- * 发送消息不可能因文件未落地而失败。
198
- */
199
- function createAtSource() {
200
- return {
201
- trigger: "@",
202
- name: "workbuddy",
203
- order: 5,
204
- showGroupTitle: true,
205
- async candidates(_session, req) {
206
- const items = await dropsList(req && req.query || "");
207
- const out = [];
208
- for (const it of items) {
209
- const isDir = it.type === "directory";
210
- out.push({
211
- name: (isDir ? "📁 " : iconFor(it.name) + " ") + it.name + (isDir ? "/" : ""),
212
- description: it.path,
213
- section: "文件缓存 · ~/.dsh-drops",
214
- value: JSON.stringify({
215
- kind: isDir ? "folder" : "file",
216
- name: it.name,
217
- path: it.path
218
- })
219
- });
132
+ /** 缓存根目录(~/.dsh-drops) */
133
+ async function dropsHome() {
134
+ try {
135
+ const body = await (await fetch("/workbuddy-drops/home")).json();
136
+ if (body.ok === true && typeof body.root === "string") return body.root;
137
+ return null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ function walkEntry(entry, rel) {
143
+ return new Promise((resolve2) => {
144
+ if (entry.isFile) {
145
+ entry.file((f) => resolve2([{
146
+ rel: rel === "" ? entry.name : rel + "/" + entry.name,
147
+ name: entry.name,
148
+ size: f.size,
149
+ file: f
150
+ }]), () => resolve2([]));
151
+ return;
220
152
  }
221
- return out.slice(0, 60);
222
- },
223
- onPick(pick) {
224
- let v = null;
225
- try {
226
- v = JSON.parse(pick.candidate.value || "null");
227
- } catch {
153
+ if (entry.isDirectory) {
154
+ const reader = entry.createReader();
155
+ const found = [];
156
+ const readBatch = () => reader.readEntries((ents) => {
157
+ if (ents.length === 0) {
158
+ Promise.all(found.map((e) => walkEntry(e, rel === "" ? entry.name : rel + "/" + entry.name))).then((rs) => resolve2(rs.flat())).catch(() => resolve2([]));
159
+ return;
160
+ }
161
+ found.push(...ents);
162
+ readBatch();
163
+ }, () => resolve2([]));
164
+ readBatch();
228
165
  return;
229
166
  }
230
- if (v === null || typeof v !== "object" || typeof v.path !== "string") return void 0;
231
- const mention = mentionFor(v.path, v.kind === "folder");
232
- return { insert: {
233
- source: "workbuddy",
234
- ref: mention,
235
- label: v.name ?? "",
236
- appearance: v.kind === "folder" ? "folder" : "file",
237
- clipboardText: mention
238
- } };
239
- },
240
- codec: {
241
- clipboardText: (ref) => ref,
242
- serialize: (ref) => Promise.resolve(ref)
167
+ resolve2([]);
168
+ });
169
+ }
170
+ async function dropsList(query) {
171
+ try {
172
+ return (await (await fetch("/workbuddy-drops/list?query=" + encodeURIComponent(query))).json()).items ?? [];
173
+ } catch {
174
+ return [];
243
175
  }
244
- };
245
- }
246
- //#endregion
247
- //#region src/client/components/file-cards.tsx
248
- /**
249
- * 对话区文件卡片(挂在 conversation.chat.turnTail 链式槽):
250
- * 用户消息发送后,该轮次消息中引用的文件以「类型图标 + 文件名 + 大小/文件夹」卡片
251
- * 渲染在轮次尾部;点击卡片经 owner 的 openFile 打开文件。
252
- * selector 只匹配含文件引用的轮次(见 definitions.ts),不抢占其他链条目。
253
- */
254
- function createFileCardsComponent(React) {
255
- function FileCard(props) {
256
- const { path, openFile } = props;
257
- const [meta, setMeta] = React.useState(null);
258
- React.useEffect(() => {
259
- let live = true;
260
- dropsStat(path).then((r) => {
261
- if (live) setMeta(r);
262
- }, () => {
263
- if (live) setMeta({
264
- ok: false,
265
- path
266
- });
267
- });
268
- return () => {
269
- live = false;
176
+ }
177
+ async function dropsStat(path) {
178
+ try {
179
+ return await (await fetch("/workbuddy-drops/stat?path=" + encodeURIComponent(path))).json();
180
+ } catch {
181
+ return {
182
+ ok: false,
183
+ path
270
184
  };
271
- }, [path]);
272
- const name = String(path).split(/[\\/]/).pop() || path;
273
- const good = meta !== null && meta !== void 0 && meta.ok === true && meta.exists === true;
274
- const isDir = good && meta.type === "directory";
275
- const icon = isDir ? "📁" : iconFor(name);
276
- const sub = meta === null ? "…" : !good ? "不可用" : isDir ? "文件夹" : formatSize(meta.size);
277
- return React.createElement("button", {
278
- type: "button",
279
- className: "wbd-card",
280
- title: path,
281
- onClick: () => {
282
- if (typeof openFile === "function") try {
283
- openFile(path);
284
- } catch {}
185
+ }
186
+ }
187
+ //#endregion
188
+ //#region src/client/at-source.ts
189
+ /**
190
+ * @ 触发源「workbuddy」:输入 @ 时在菜单中追加「文件缓存」分组 ——
191
+ * ~/.dsh-drops 中拖拽/粘贴/选择落地的文件与目录树,支持搜索。
192
+ * 选中后由输入管线的 onPick → { insert } 在触发词位置铸造原生气泡。
193
+ *
194
+ * 工作区文件/文件夹的 @ 检索由 DSH 官方 ui-reference 源提供(文件与文件夹 +
195
+ * Session 分组),本插件与之共存,无需重复实现。
196
+ *
197
+ * 所有引用在拖入时已完成落地(真实绝对路径),因此 codec 序列化是恒等函数,
198
+ * 发送消息不可能因文件未落地而失败。
199
+ */
200
+ function createAtSource() {
201
+ return {
202
+ trigger: "@",
203
+ name: "workbuddy",
204
+ order: 5,
205
+ showGroupTitle: true,
206
+ async candidates(_session, req) {
207
+ const items = await dropsList(req && req.query || "");
208
+ const out = [];
209
+ for (const it of items) {
210
+ const isDir = it.type === "directory";
211
+ out.push({
212
+ name: (isDir ? "📁 " : iconFor(it.name) + " ") + it.name + (isDir ? "/" : ""),
213
+ description: it.path,
214
+ section: "文件缓存 · ~/.dsh-drops",
215
+ value: JSON.stringify({
216
+ kind: isDir ? "folder" : "file",
217
+ name: it.name,
218
+ path: it.path
219
+ })
220
+ });
221
+ }
222
+ return out.slice(0, 60);
223
+ },
224
+ onPick(pick) {
225
+ let v = null;
226
+ try {
227
+ v = JSON.parse(pick.candidate.value || "null");
228
+ } catch {
229
+ return;
230
+ }
231
+ if (v === null || typeof v !== "object" || typeof v.path !== "string") return void 0;
232
+ const mention = mentionFor(v.path, v.kind === "folder");
233
+ return { insert: {
234
+ source: "workbuddy",
235
+ ref: mention,
236
+ label: v.name ?? "",
237
+ appearance: v.kind === "folder" ? "folder" : "file",
238
+ clipboardText: mention
239
+ } };
240
+ },
241
+ codec: {
242
+ clipboardText: (ref) => ref,
243
+ serialize: (ref) => Promise.resolve(ref)
285
244
  }
286
- }, React.createElement("span", { className: "wbd-card-icon" }, icon), React.createElement("span", { className: "wbd-card-name" }, name), React.createElement("span", { className: "wbd-card-sub" }, sub));
245
+ };
287
246
  }
288
- return function FileCards(props) {
289
- const matched = props.matched;
290
- if (matched === null || matched === void 0 || !Array.isArray(matched.refs) || matched.refs.length === 0) return null;
291
- return React.createElement("div", { className: "wbd-cards" }, React.createElement("span", { className: "wbd-cards-label" }, "📎 消息引用的文件"), matched.refs.map((path, i) => React.createElement(FileCard, {
292
- key: String(path) + ":" + i,
293
- path,
294
- openFile: props.openFile
295
- })));
296
- };
297
- }
298
- //#endregion
299
- //#region src/client/components/overlay.tsx
300
- /**
301
- * 全屏拖拽遮罩 + toast(挂在 shell.overlay 列表槽)。
302
- * 遮罩只在拖拽含非图片文件/文件夹时出现(纯图片拖放交给原生图片轨道);
303
- * 层本身点击穿透,遮罩激活时开启 pointer-events 承接 drop。
304
- */
305
- function createOverlayComponent(React, bus) {
306
- return function WorkbuddyOverlay() {
307
- const [state, setState] = React.useState(bus.get());
308
- React.useEffect(() => bus.subscribe(setState), []);
309
- return React.createElement("div", { className: "wbd-overlay" }, state.active ? React.createElement("div", { className: "wbd-shield" }, React.createElement("div", { className: "wbd-shield-inner" }, React.createElement("div", { className: "wbd-shield-icon" }, "📥"), React.createElement("div", { className: "wbd-shield-title" }, "松开以接收文件"), React.createElement("div", { className: "wbd-shield-sub" }, state.count + " 项 · 将作为引用气泡插入输入框光标处"), React.createElement("div", { className: "wbd-shield-hint" }, "文件(含图片)将缓存至 ~/.dsh-drops 并引用绝对路径"))) : null, state.toast ? React.createElement("div", { className: "wbd-toast" + (state.toast.level === "error" ? " wbd-error" : "") }, state.toast.text) : null);
310
- };
311
- }
312
- //#endregion
313
- //#region src/client/components/pick-button.tsx
314
- /**
315
- * 📎 引用按钮(挂在 conversation.input.left 列表槽):
316
- * 统一走 <input type=file>(多选 / webkitdirectory)→ 立即插入气泡 →
317
- * 后台缓存(所有浏览器行为一致)。
318
- */
319
- function createPickButtonComponent(React, bus, handlers) {
320
- return function PickButton() {
321
- const [open, setOpen] = React.useState(false);
322
- const newBatch = () => "drop-" + Date.now().toString(36);
323
- const pickFolder = async () => {
324
- setOpen(false);
325
- const input = document.createElement("input");
326
- input.type = "file";
327
- input.setAttribute("webkitdirectory", "");
328
- input.onchange = () => {
329
- const files = [];
330
- const tops = [];
331
- for (const f of Array.from(input.files ?? [])) {
332
- files.push({
333
- rel: f.webkitRelativePath || f.name,
247
+ //#endregion
248
+ //#region src/client/components/file-cards.tsx
249
+ /**
250
+ * 对话区文件卡片(挂在 conversation.chat.turnTail 链式槽):
251
+ * 用户消息发送后,该轮次消息中引用的文件以「类型图标 + 文件名 + 大小/文件夹」卡片
252
+ * 渲染在轮次尾部;点击卡片经 owner 的 openFile 打开文件。
253
+ * selector 只匹配含文件引用的轮次(见 definitions.ts),不抢占其他链条目。
254
+ */
255
+ function createFileCardsComponent(React) {
256
+ function FileCard(props) {
257
+ const { path, openFile } = props;
258
+ const [meta, setMeta] = React.useState(null);
259
+ React.useEffect(() => {
260
+ let live = true;
261
+ dropsStat(path).then((r) => {
262
+ if (live) setMeta(r);
263
+ }, () => {
264
+ if (live) setMeta({
265
+ ok: false,
266
+ path
267
+ });
268
+ });
269
+ return () => {
270
+ live = false;
271
+ };
272
+ }, [path]);
273
+ const name = String(path).split(/[\\/]/).pop() || path;
274
+ const good = meta !== null && meta !== void 0 && meta.ok === true && meta.exists === true;
275
+ const isDir = good && meta.type === "directory";
276
+ const icon = isDir ? "📁" : iconFor(name);
277
+ const sub = meta === null ? "…" : !good ? "不可用" : isDir ? "文件夹" : formatSize(meta.size);
278
+ return React.createElement("button", {
279
+ type: "button",
280
+ className: "wbd-card",
281
+ title: path,
282
+ onClick: () => {
283
+ if (typeof openFile === "function") try {
284
+ openFile(path);
285
+ } catch {}
286
+ }
287
+ }, React.createElement("span", { className: "wbd-card-icon" }, icon), React.createElement("span", { className: "wbd-card-name" }, name), React.createElement("span", { className: "wbd-card-sub" }, sub));
288
+ }
289
+ return function FileCards(props) {
290
+ const matched = props.matched;
291
+ if (matched === null || matched === void 0 || !Array.isArray(matched.refs) || matched.refs.length === 0) return null;
292
+ return React.createElement("div", { className: "wbd-cards" }, React.createElement("span", { className: "wbd-cards-label" }, "📎 消息引用的文件"), matched.refs.map((path, i) => React.createElement(FileCard, {
293
+ key: String(path) + ":" + i,
294
+ path,
295
+ openFile: props.openFile
296
+ })));
297
+ };
298
+ }
299
+ //#endregion
300
+ //#region src/client/components/overlay.tsx
301
+ /**
302
+ * 全屏拖拽遮罩 + toast(挂在 shell.overlay 列表槽)。
303
+ * 遮罩只在拖拽含非图片文件/文件夹时出现(纯图片拖放交给原生图片轨道);
304
+ * 层本身点击穿透,遮罩激活时开启 pointer-events 承接 drop。
305
+ */
306
+ function createOverlayComponent(React, bus) {
307
+ return function WorkbuddyOverlay() {
308
+ const [state, setState] = React.useState(bus.get());
309
+ React.useEffect(() => bus.subscribe(setState), []);
310
+ return React.createElement("div", { className: "wbd-overlay" }, state.active ? React.createElement("div", { className: "wbd-shield" }, React.createElement("div", { className: "wbd-shield-inner" }, React.createElement("div", { className: "wbd-shield-icon" }, "📥"), React.createElement("div", { className: "wbd-shield-title" }, "松开以接收文件"), React.createElement("div", { className: "wbd-shield-sub" }, state.count + " 项 · 将作为引用气泡插入输入框光标处"), React.createElement("div", { className: "wbd-shield-hint" }, "文件(含图片)将缓存至 ~/.dsh-drops 并引用绝对路径"))) : null, state.toast ? React.createElement("div", { className: "wbd-toast" + (state.toast.level === "error" ? " wbd-error" : "") }, state.toast.text) : null);
311
+ };
312
+ }
313
+ //#endregion
314
+ //#region src/client/components/pick-button.tsx
315
+ /**
316
+ * 📎 引用按钮(挂在 conversation.input.left 列表槽):
317
+ * 统一走 <input type=file>(多选 / webkitdirectory)→ 立即插入气泡 →
318
+ * 后台缓存(所有浏览器行为一致)。
319
+ */
320
+ function createPickButtonComponent(React, bus, handlers) {
321
+ return function PickButton() {
322
+ const [open, setOpen] = React.useState(false);
323
+ const newBatch = () => "drop-" + Date.now().toString(36);
324
+ const pickFolder = async () => {
325
+ setOpen(false);
326
+ const input = document.createElement("input");
327
+ input.type = "file";
328
+ input.setAttribute("webkitdirectory", "");
329
+ input.onchange = () => {
330
+ const files = [];
331
+ const tops = [];
332
+ for (const f of Array.from(input.files ?? [])) {
333
+ files.push({
334
+ rel: f.webkitRelativePath || f.name,
335
+ name: f.name,
336
+ size: f.size,
337
+ file: f
338
+ });
339
+ const top = (f.webkitRelativePath || "").split("/")[0];
340
+ if (top !== "" && !tops.includes(top)) tops.push(top);
341
+ }
342
+ if (files.length > 0) handlers.acceptTree(files, tops, newBatch());
343
+ };
344
+ input.click();
345
+ };
346
+ const pickFiles = async () => {
347
+ setOpen(false);
348
+ const input = document.createElement("input");
349
+ input.type = "file";
350
+ input.multiple = true;
351
+ input.onchange = () => {
352
+ const files = [];
353
+ for (const f of Array.from(input.files ?? [])) files.push({
354
+ rel: f.name,
334
355
  name: f.name,
335
356
  size: f.size,
336
357
  file: f
337
358
  });
338
- const top = (f.webkitRelativePath || "").split("/")[0];
339
- if (top !== "" && !tops.includes(top)) tops.push(top);
340
- }
341
- if (files.length > 0) handlers.acceptTree(files, tops, newBatch());
359
+ if (files.length > 0) handlers.acceptTree(files, [], newBatch());
360
+ };
361
+ input.click();
342
362
  };
343
- input.click();
363
+ return React.createElement("div", { className: "wbd-pick" }, React.createElement("button", {
364
+ type: "button",
365
+ className: "wbd-pick-btn",
366
+ title: "引用文件/文件夹(也可直接拖拽或粘贴)",
367
+ onClick: () => setOpen(!open)
368
+ }, "📎"), open ? React.createElement("div", { className: "wbd-pick-menu" }, React.createElement("button", {
369
+ type: "button",
370
+ onClick: pickFiles
371
+ }, "选择文件…"), React.createElement("button", {
372
+ type: "button",
373
+ onClick: pickFolder
374
+ }, "选择文件夹…(保留目录树)")) : null);
344
375
  };
345
- const pickFiles = async () => {
346
- setOpen(false);
347
- const input = document.createElement("input");
348
- input.type = "file";
349
- input.multiple = true;
350
- input.onchange = () => {
351
- const files = [];
352
- for (const f of Array.from(input.files ?? [])) files.push({
353
- rel: f.name,
354
- name: f.name,
355
- size: f.size,
356
- file: f
357
- });
358
- if (files.length > 0) handlers.acceptTree(files, [], newBatch());
376
+ }
377
+ //#endregion
378
+ //#region src/client/css.ts
379
+ /** 包内样式(styles.insert 注入,随插件 Run 生命周期清理) */
380
+ const CSS = [
381
+ ".wbd-overlay{position:fixed;inset:0;pointer-events:none;z-index:2147483000}",
382
+ ".wbd-shield{position:fixed;inset:0;pointer-events:auto;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-color-bg,#0e1116) 72%,transparent);backdrop-filter:blur(2px)}",
383
+ ".wbd-shield-inner{min-width:340px;max-width:520px;padding:28px 36px;text-align:center;border:2px dashed var(--dsw-alias-border-l3,#4c9aff);border-radius:14px;background:var(--dsw-color-bg-elevated,#161b24);box-shadow:0 12px 48px rgba(0,0,0,.45)}",
384
+ ".wbd-shield-icon{font-size:34px;line-height:1}",
385
+ ".wbd-shield-title{margin-top:10px;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,#f2f4f8)}",
386
+ ".wbd-shield-sub{margin-top:6px;font-size:13px;color:var(--dsw-alias-label-secondary,#aab2c0)}",
387
+ ".wbd-shield-hint{margin-top:10px;font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
388
+ ".wbd-toast{position:fixed;right:24px;bottom:132px;max-width:360px;padding:9px 14px;border-radius:10px;background:var(--dsw-color-bg-elevated,#1c222e);border:1px solid var(--dsw-alias-border-l3,#3a4252);color:var(--dsw-alias-label-primary,#f2f4f8);font-size:13px;box-shadow:0 8px 28px rgba(0,0,0,.35);pointer-events:auto}",
389
+ ".wbd-toast.wbd-error{border-color:#b3453f}",
390
+ ".wbd-cards{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:14px 0 4px;font-size:13px}",
391
+ ".wbd-cards-label{color:var(--dsw-alias-label-tertiary,#6b7280);margin-right:4px}",
392
+ ".wbd-card{display:inline-flex;align-items:center;gap:7px;max-width:340px;padding:4px 10px 4px 8px;border:1px solid var(--dsw-alias-border-l2,#2c3342);border-radius:8px;background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.04));color:var(--dsw-alias-label-primary,#f2f4f8);font:inherit;font-size:13px;cursor:pointer;text-align:left}",
393
+ ".wbd-card:hover{border-color:var(--dsw-alias-border-l3,#4c9aff)}",
394
+ ".wbd-card-icon{flex:none;font-size:15px}",
395
+ ".wbd-card-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
396
+ ".wbd-card-sub{flex:none;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:12px}",
397
+ ".wbd-pick{position:relative;display:inline-flex}",
398
+ ".wbd-pick-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,#aab2c0);font-size:15px;cursor:pointer}",
399
+ ".wbd-pick-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06));color:var(--dsw-alias-label-primary,#f2f4f8)}",
400
+ ".wbd-pick-menu{position:absolute;left:0;bottom:calc(100% + 8px);display:flex;flex-direction:column;min-width:220px;padding:6px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2,#2c3342);background:var(--dsw-color-bg-elevated,#1c222e);box-shadow:0 10px 32px rgba(0,0,0,.4);z-index:10}",
401
+ ".wbd-pick-menu button{display:block;width:100%;padding:8px 10px;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary,#f2f4f8);font:inherit;font-size:13px;text-align:left;cursor:pointer}",
402
+ ".wbd-pick-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06))}",
403
+ "/* ---- 引用气泡换肤:主题色圆角矩形(原子删除由输入机原生保证) ---- */",
404
+ "/* 关键:padding 用等量负 margin 抵消、描边用 box-shadow(不占布局)—— */",
405
+ "/* 气泡外部宽度与 textarea 字符宽度完全一致,backdrop 与光标严格对齐 */",
406
+ "[data-decoration=\"chip\"]{padding:0 8px !important;margin:0 -8px !important;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4c9aff) 14%,transparent) !important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary,#4c9aff) 45%,transparent);color:var(--dsw-alias-brand-primary,#4c9aff) !important;font-weight:500}",
407
+ "[data-decoration=\"chip\"]:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4c9aff) 22%,transparent) !important}",
408
+ "[data-decoration=\"chip\"][data-invalid=\"true\"]{color:var(--dsw-alias-state-error-primary,#e56a64) !important;background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e56a64) 14%,transparent) !important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dsw-alias-state-error-primary,#e56a64) 45%,transparent) !important}",
409
+ "[data-decoration=\"chip\"] [class*=\"chipTriggerGlyph\"]{font-size:12px;opacity:.8}"
410
+ ].join("\n");
411
+ //#endregion
412
+ //#region src/client/definitions.ts
413
+ const boundaryDef = {
414
+ kind: "workbuddy-turn-boundary",
415
+ match: (event) => event.type === "turn/start" ? {
416
+ id: String(event.data.turn),
417
+ role: "start"
418
+ } : event.type === "turn/end" ? {
419
+ id: String(event.data.turn),
420
+ role: "update"
421
+ } : null,
422
+ start: (_context, match) => {
423
+ if (match.event.type !== "turn/start") throw new Error("workbuddy-turn-boundary start requires turn/start");
424
+ return {
425
+ turn: match.event.data.turn,
426
+ startSeq: match.event.seq
359
427
  };
360
- input.click();
361
- };
362
- return React.createElement("div", { className: "wbd-pick" }, React.createElement("button", {
363
- type: "button",
364
- className: "wbd-pick-btn",
365
- title: "引用文件/文件夹(也可直接拖拽或粘贴)",
366
- onClick: () => setOpen(!open)
367
- }, "📎"), open ? React.createElement("div", { className: "wbd-pick-menu" }, React.createElement("button", {
368
- type: "button",
369
- onClick: pickFiles
370
- }, "选择文件…"), React.createElement("button", {
371
- type: "button",
372
- onClick: pickFolder
373
- }, "选择文件夹…(保留目录树)")) : null);
428
+ },
429
+ update: (context) => context.state
374
430
  };
375
- }
376
- //#endregion
377
- //#region src/client/css.ts
378
- /** 包内样式(styles.insert 注入,随插件 Run 生命周期清理) */
379
- const CSS = [
380
- ".wbd-overlay{position:fixed;inset:0;pointer-events:none;z-index:2147483000}",
381
- ".wbd-shield{position:fixed;inset:0;pointer-events:auto;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-color-bg,#0e1116) 72%,transparent);backdrop-filter:blur(2px)}",
382
- ".wbd-shield-inner{min-width:340px;max-width:520px;padding:28px 36px;text-align:center;border:2px dashed var(--dsw-alias-border-l3,#4c9aff);border-radius:14px;background:var(--dsw-color-bg-elevated,#161b24);box-shadow:0 12px 48px rgba(0,0,0,.45)}",
383
- ".wbd-shield-icon{font-size:34px;line-height:1}",
384
- ".wbd-shield-title{margin-top:10px;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,#f2f4f8)}",
385
- ".wbd-shield-sub{margin-top:6px;font-size:13px;color:var(--dsw-alias-label-secondary,#aab2c0)}",
386
- ".wbd-shield-hint{margin-top:10px;font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
387
- ".wbd-toast{position:fixed;right:24px;bottom:132px;max-width:360px;padding:9px 14px;border-radius:10px;background:var(--dsw-color-bg-elevated,#1c222e);border:1px solid var(--dsw-alias-border-l3,#3a4252);color:var(--dsw-alias-label-primary,#f2f4f8);font-size:13px;box-shadow:0 8px 28px rgba(0,0,0,.35);pointer-events:auto}",
388
- ".wbd-toast.wbd-error{border-color:#b3453f}",
389
- ".wbd-cards{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:14px 0 4px;font-size:13px}",
390
- ".wbd-cards-label{color:var(--dsw-alias-label-tertiary,#6b7280);margin-right:4px}",
391
- ".wbd-card{display:inline-flex;align-items:center;gap:7px;max-width:340px;padding:4px 10px 4px 8px;border:1px solid var(--dsw-alias-border-l2,#2c3342);border-radius:8px;background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.04));color:var(--dsw-alias-label-primary,#f2f4f8);font:inherit;font-size:13px;cursor:pointer;text-align:left}",
392
- ".wbd-card:hover{border-color:var(--dsw-alias-border-l3,#4c9aff)}",
393
- ".wbd-card-icon{flex:none;font-size:15px}",
394
- ".wbd-card-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
395
- ".wbd-card-sub{flex:none;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:12px}",
396
- ".wbd-pick{position:relative;display:inline-flex}",
397
- ".wbd-pick-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,#aab2c0);font-size:15px;cursor:pointer}",
398
- ".wbd-pick-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06));color:var(--dsw-alias-label-primary,#f2f4f8)}",
399
- ".wbd-pick-menu{position:absolute;left:0;bottom:calc(100% + 8px);display:flex;flex-direction:column;min-width:220px;padding:6px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2,#2c3342);background:var(--dsw-color-bg-elevated,#1c222e);box-shadow:0 10px 32px rgba(0,0,0,.4);z-index:10}",
400
- ".wbd-pick-menu button{display:block;width:100%;padding:8px 10px;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary,#f2f4f8);font:inherit;font-size:13px;text-align:left;cursor:pointer}",
401
- ".wbd-pick-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06))}",
402
- "/* ---- 引用气泡换肤:主题色圆角矩形(原子删除由输入机原生保证) ---- */",
403
- "/* 关键:padding 用等量负 margin 抵消、描边用 box-shadow(不占布局)—— */",
404
- "/* 气泡外部宽度与 textarea 字符宽度完全一致,backdrop 与光标严格对齐 */",
405
- "[data-decoration=\"chip\"]{padding:0 8px !important;margin:0 -8px !important;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4c9aff) 14%,transparent) !important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary,#4c9aff) 45%,transparent);color:var(--dsw-alias-brand-primary,#4c9aff) !important;font-weight:500}",
406
- "[data-decoration=\"chip\"]:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4c9aff) 22%,transparent) !important}",
407
- "[data-decoration=\"chip\"][data-invalid=\"true\"]{color:var(--dsw-alias-state-error-primary,#e56a64) !important;background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e56a64) 14%,transparent) !important;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dsw-alias-state-error-primary,#e56a64) 45%,transparent) !important}",
408
- "[data-decoration=\"chip\"] [class*=\"chipTriggerGlyph\"]{font-size:12px;opacity:.8}"
409
- ].join("\n");
410
- //#endregion
411
- //#region src/client/definitions.ts
412
- const boundaryDef = {
413
- kind: "workbuddy-turn-boundary",
414
- match: (event) => event.type === "turn/start" ? {
415
- id: String(event.data.turn),
416
- role: "start"
417
- } : event.type === "turn/end" ? {
418
- id: String(event.data.turn),
419
- role: "update"
420
- } : null,
421
- start: (_context, match) => {
422
- if (match.event.type !== "turn/start") throw new Error("workbuddy-turn-boundary start requires turn/start");
423
- return {
424
- turn: match.event.data.turn,
425
- startSeq: match.event.seq
426
- };
427
- },
428
- update: (context) => context.state
429
- };
430
- const fileRefsDef = {
431
- kind: "workbuddy-file-refs",
432
- match: (event) => {
433
- if (event.type === "user/message" && event.data?.source?.kind === "user" && event.data.id !== void 0) return {
434
- id: String(event.data.id),
435
- role: "start"
436
- };
437
- if (event.type === "turn/start" && event.data.turn !== void 0) return {
438
- id: "turn-" + String(event.data.turn),
439
- role: "start"
440
- };
441
- return null;
442
- },
443
- start: (context, match, reader) => {
444
- if (match.event.type === "turn/start") {
445
- let refs = [];
446
- const prev = reader.previous("workbuddy-file-refs");
447
- const prevBoundary = reader.previous("workbuddy-turn-boundary");
448
- if (prev !== void 0 && prev.state !== void 0 && Array.isArray(prev.state.refs)) {
449
- if (prevBoundary === void 0 || prevBoundary.state === void 0 || prev.startSeq > prevBoundary.startSeq) refs = prev.state.refs;
431
+ const fileRefsDef = {
432
+ kind: "workbuddy-file-refs",
433
+ match: (event) => {
434
+ if (event.type === "user/message" && event.data?.source?.kind === "user" && event.data.id !== void 0) return {
435
+ id: String(event.data.id),
436
+ role: "start"
437
+ };
438
+ if (event.type === "turn/start" && event.data.turn !== void 0) return {
439
+ id: "turn-" + String(event.data.turn),
440
+ role: "start"
441
+ };
442
+ return null;
443
+ },
444
+ start: (context, match, reader) => {
445
+ if (match.event.type === "turn/start") {
446
+ let refs = [];
447
+ const prev = reader.previous("workbuddy-file-refs");
448
+ const prevBoundary = reader.previous("workbuddy-turn-boundary");
449
+ if (prev !== void 0 && prev.state !== void 0 && Array.isArray(prev.state.refs)) {
450
+ if (prevBoundary === void 0 || prevBoundary.state === void 0 || prev.startSeq > prevBoundary.startSeq) refs = prev.state.refs;
451
+ }
452
+ return {
453
+ turn: match.event.data.turn,
454
+ refs
455
+ };
450
456
  }
457
+ const ev = match.event;
451
458
  return {
452
- turn: match.event.data.turn,
453
- refs
459
+ seq: ev.seq,
460
+ refs: extractRefs(ev.data?.content ?? [])
461
+ };
462
+ },
463
+ update: (context) => context.state,
464
+ buildLocationData: (context, scope) => {
465
+ if (scope !== "turn") return null;
466
+ const s = context.state;
467
+ if (s === void 0 || s.turn === void 0 || !Array.isArray(s.refs) || s.refs.length === 0) return null;
468
+ return {
469
+ kind: "turn",
470
+ turn: s.turn,
471
+ key: "workbuddy-file-refs",
472
+ value: { refs: s.refs }
454
473
  };
455
474
  }
456
- const ev = match.event;
457
- return {
458
- seq: ev.seq,
459
- refs: extractRefs(ev.data?.content ?? [])
475
+ };
476
+ /** turnTail chain selector:仅当该轮次存在文件引用时返回 matched,避免抢占其他链条目 */
477
+ function selectTurnFileRefs(owner) {
478
+ const t = owner !== null && owner !== void 0 ? owner.turn : void 0;
479
+ if (t === void 0 || t.data === void 0 || typeof t.data.get !== "function") return null;
480
+ const data = t.data.get("workbuddy-file-refs");
481
+ if (data === void 0 || data === null || !Array.isArray(data.refs) || data.refs.length === 0) return null;
482
+ return { refs: data.refs };
483
+ }
484
+ //#endregion
485
+ //#region src/client/lib/bus.ts
486
+ function createDropBus() {
487
+ let state = {
488
+ active: false,
489
+ count: 0,
490
+ toast: null
460
491
  };
461
- },
462
- update: (context) => context.state,
463
- buildLocationData: (context, scope) => {
464
- if (scope !== "turn") return null;
465
- const s = context.state;
466
- if (s === void 0 || s.turn === void 0 || !Array.isArray(s.refs) || s.refs.length === 0) return null;
467
- return {
468
- kind: "turn",
469
- turn: s.turn,
470
- key: "workbuddy-file-refs",
471
- value: { refs: s.refs }
492
+ const subs = /* @__PURE__ */ new Set();
493
+ let timer = null;
494
+ const get = () => state;
495
+ const set = (next) => {
496
+ state = next;
497
+ for (const fn of subs) fn(state);
472
498
  };
473
- }
474
- };
475
- /** turnTail chain selector:仅当该轮次存在文件引用时返回 matched,避免抢占其他链条目 */
476
- function selectTurnFileRefs(owner) {
477
- const t = owner !== null && owner !== void 0 ? owner.turn : void 0;
478
- if (t === void 0 || t.data === void 0 || typeof t.data.get !== "function") return null;
479
- const data = t.data.get("workbuddy-file-refs");
480
- if (data === void 0 || data === null || !Array.isArray(data.refs) || data.refs.length === 0) return null;
481
- return { refs: data.refs };
482
- }
483
- //#endregion
484
- //#region src/client/lib/bus.ts
485
- function createDropBus() {
486
- let state = {
487
- active: false,
488
- count: 0,
489
- toast: null
490
- };
491
- const subs = /* @__PURE__ */ new Set();
492
- let timer = null;
493
- const get = () => state;
494
- const set = (next) => {
495
- state = next;
496
- for (const fn of subs) fn(state);
497
- };
498
- const subscribe = (fn) => {
499
- subs.add(fn);
500
- return () => {
501
- subs.delete(fn);
499
+ const subscribe = (fn) => {
500
+ subs.add(fn);
501
+ return () => {
502
+ subs.delete(fn);
503
+ };
502
504
  };
503
- };
504
- const toast = (text, level = "info") => {
505
- if (timer !== null) clearTimeout(timer);
506
- set({
507
- ...state,
508
- toast: {
509
- text: String(text),
510
- level
511
- }
512
- });
513
- timer = setTimeout(() => {
514
- timer = null;
505
+ const toast = (text, level = "info") => {
506
+ if (timer !== null) clearTimeout(timer);
515
507
  set({
516
508
  ...state,
517
- toast: null
509
+ toast: {
510
+ text: String(text),
511
+ level
512
+ }
518
513
  });
519
- }, 4600);
520
- };
521
- return {
522
- get,
523
- set,
524
- subscribe,
525
- toast
526
- };
527
- }
528
- //#endregion
529
- //#region src/client/lib/drop.ts
530
- /** 任何文件拖拽都接管;items 不可用(Firefox dragenter/dragover)时按 types 兜底 */
531
- function interceptable(dt) {
532
- if (dt === null || dt === void 0) return false;
533
- if (Array.from(dt.types ?? []).includes("Files")) return true;
534
- if (dt.items) {
535
- for (const it of Array.from(dt.items)) if (it.kind === "file") return true;
514
+ timer = setTimeout(() => {
515
+ timer = null;
516
+ set({
517
+ ...state,
518
+ toast: null
519
+ });
520
+ }, 4600);
521
+ };
522
+ return {
523
+ get,
524
+ set,
525
+ subscribe,
526
+ toast
527
+ };
536
528
  }
537
- return false;
538
- }
539
- function countFiles(dt) {
540
- let n = 0;
541
- for (const it of Array.from(dt.items)) if (it.kind === "file") n += 1;
542
- return n;
543
- }
544
- /**
545
- * 关键:DataTransfer 只在事件同步阶段有效 —— getAsFile / webkitGetAsEntry
546
- * 必须在事件处理器内、任何 await 之前完成调用,否则浏览器清空 DataTransfer
547
- * 后全部返回 null。本函数专门在同步阶段收集 File / Entry。
548
- */
549
- function syncCollect(dt) {
550
- const synced = [];
551
- for (const it of Array.from(dt.items)) {
552
- if (it.kind !== "file") continue;
553
- let entry = null;
554
- try {
555
- const getter = it.webkitGetAsEntry ?? it.getAsEntry;
556
- entry = typeof getter === "function" ? getter() : null;
557
- } catch {
558
- entry = null;
559
- }
560
- let file = null;
561
- try {
562
- file = typeof it.getAsFile === "function" ? it.getAsFile() : null;
563
- } catch {
564
- file = null;
529
+ //#endregion
530
+ //#region src/client/lib/drop.ts
531
+ /** 任何文件拖拽都接管;items 不可用(Firefox dragenter/dragover)时按 types 兜底 */
532
+ function interceptable(dt) {
533
+ if (dt === null || dt === void 0) return false;
534
+ if (Array.from(dt.types ?? []).includes("Files")) return true;
535
+ if (dt.items) {
536
+ for (const it of Array.from(dt.items)) if (it.kind === "file") return true;
565
537
  }
566
- if (entry === null && file === null) continue;
567
- synced.push({
568
- entry,
569
- file
570
- });
538
+ return false;
539
+ }
540
+ function countFiles(dt) {
541
+ let n = 0;
542
+ for (const it of Array.from(dt.items)) if (it.kind === "file") n += 1;
543
+ return n;
571
544
  }
572
- return synced;
573
- }
574
- function createDropHandlers(deps) {
575
- const { bus, insert } = deps;
576
- const submitRefs = async (refs, jobs, batch) => {
577
- if (refs.length === 0) {
578
- bus.toast("无法读取拖入的内容", "error");
579
- return;
580
- }
581
- const inserted = await insert(refs);
582
- if (inserted > 0) bus.toast("已引用 " + inserted + " 项,文件正在后台缓存");
583
- else bus.toast("未能插入引用(见上方提示)", "error");
584
- deps.enqueueUpload(jobs, batch);
585
- };
586
- /** 文件选择框路径(已有 TreeFile 列表) */
587
- const acceptTree = async (files, dirTops, batch) => {
588
- const root = await deps.ensureRoot();
589
- if (root === null) {
590
- bus.toast("无法获取缓存目录,请重试", "error");
591
- return;
592
- }
593
- const refs = [];
594
- for (const top of dirTops) {
595
- const dirPath = root + "/" + batch + "/" + top;
596
- refs.push({
597
- label: top,
598
- reference: {
599
- source: "workbuddy",
600
- ref: mentionFor(dirPath, true),
601
- label: top,
602
- appearance: "folder",
603
- clipboardText: mentionFor(dirPath, true)
604
- }
605
- });
606
- }
607
- for (const f of files) {
608
- const path = root + "/" + batch + "/" + f.rel;
609
- refs.push({
610
- label: f.name,
611
- reference: {
612
- source: "workbuddy",
613
- ref: mentionFor(path, false),
614
- label: f.name,
615
- appearance: "file",
616
- clipboardText: mentionFor(path, false)
617
- }
618
- });
619
- }
620
- await submitRefs(refs, files.map((f) => ({
621
- kind: "file",
622
- file: f.file,
623
- rel: f.rel,
624
- name: f.name
625
- })), batch);
626
- };
627
545
  /**
628
- * 核心:先按预分配路径立即插入气泡,再交给后台缓存。
629
- * synced 必须在事件内同步收集完毕(见 syncCollect)。
546
+ * 关键:DataTransfer 只在事件同步阶段有效 —— getAsFile / webkitGetAsEntry
547
+ * 必须在事件处理器内、任何 await 之前完成调用,否则浏览器清空 DataTransfer
548
+ * 后全部返回 null。本函数专门在同步阶段收集 File / Entry。
630
549
  */
631
- const acceptAndInsert = async (synced, batch) => {
632
- const root = await deps.ensureRoot();
633
- if (root === null) {
634
- bus.toast("无法获取缓存目录,请重试", "error");
635
- return;
550
+ function syncCollect(dt) {
551
+ const synced = [];
552
+ for (const it of Array.from(dt.items)) {
553
+ if (it.kind !== "file") continue;
554
+ let entry = null;
555
+ try {
556
+ const getter = it.webkitGetAsEntry ?? it.getAsEntry;
557
+ entry = typeof getter === "function" ? getter() : null;
558
+ } catch {
559
+ entry = null;
560
+ }
561
+ let file = null;
562
+ try {
563
+ file = typeof it.getAsFile === "function" ? it.getAsFile() : null;
564
+ } catch {
565
+ file = null;
566
+ }
567
+ if (entry === null && file === null) continue;
568
+ synced.push({
569
+ entry,
570
+ file
571
+ });
636
572
  }
637
- const refs = [];
638
- const jobs = [];
639
- for (const s of synced) {
640
- const { entry, file } = s;
641
- if (entry !== null && entry !== void 0 && entry.isDirectory) {
642
- const top = entry.name;
573
+ return synced;
574
+ }
575
+ function createDropHandlers(deps) {
576
+ const { bus, insert } = deps;
577
+ const submitRefs = async (refs, jobs, batch) => {
578
+ if (refs.length === 0) {
579
+ bus.toast("无法读取拖入的内容", "error");
580
+ return;
581
+ }
582
+ const inserted = await insert(refs);
583
+ if (inserted > 0) bus.toast("已引用 " + inserted + " 项,文件正在后台缓存");
584
+ else bus.toast("未能插入引用(见上方提示)", "error");
585
+ deps.enqueueUpload(jobs, batch);
586
+ };
587
+ /** 文件选择框路径(已有 TreeFile 列表) */
588
+ const acceptTree = async (files, dirTops, batch) => {
589
+ const root = await deps.ensureRoot();
590
+ if (root === null) {
591
+ bus.toast("无法获取缓存目录,请重试", "error");
592
+ return;
593
+ }
594
+ const refs = [];
595
+ for (const top of dirTops) {
643
596
  const dirPath = root + "/" + batch + "/" + top;
644
597
  refs.push({
645
598
  label: top,
@@ -651,334 +604,382 @@ function createDropHandlers(deps) {
651
604
  clipboardText: mentionFor(dirPath, true)
652
605
  }
653
606
  });
654
- jobs.push({
655
- kind: "dir",
656
- entry
657
- });
658
- continue;
659
607
  }
660
- if (file !== null && file !== void 0) {
661
- const rel = file.name;
662
- const path = root + "/" + batch + "/" + rel;
608
+ for (const f of files) {
609
+ const path = root + "/" + batch + "/" + f.rel;
663
610
  refs.push({
664
- label: file.name,
611
+ label: f.name,
665
612
  reference: {
666
613
  source: "workbuddy",
667
614
  ref: mentionFor(path, false),
668
- label: file.name,
615
+ label: f.name,
669
616
  appearance: "file",
670
617
  clipboardText: mentionFor(path, false)
671
618
  }
672
619
  });
673
- jobs.push({
674
- kind: "file",
675
- file,
676
- rel,
677
- name: file.name
678
- });
679
- continue;
680
620
  }
681
- if (entry !== null && entry !== void 0 && entry.isFile) {
682
- const fe = entry;
683
- const f = await new Promise((resolve2) => fe.file((ff) => resolve2(ff), () => resolve2(null)));
684
- if (f !== null && f !== void 0) {
685
- const rel = f.name;
621
+ await submitRefs(refs, files.map((f) => ({
622
+ kind: "file",
623
+ file: f.file,
624
+ rel: f.rel,
625
+ name: f.name
626
+ })), batch);
627
+ };
628
+ /**
629
+ * 核心:先按预分配路径立即插入气泡,再交给后台缓存。
630
+ * synced 必须在事件内同步收集完毕(见 syncCollect)。
631
+ */
632
+ const acceptAndInsert = async (synced, batch) => {
633
+ const root = await deps.ensureRoot();
634
+ if (root === null) {
635
+ bus.toast("无法获取缓存目录,请重试", "error");
636
+ return;
637
+ }
638
+ const refs = [];
639
+ const jobs = [];
640
+ for (const s of synced) {
641
+ const { entry, file } = s;
642
+ if (entry !== null && entry !== void 0 && entry.isDirectory) {
643
+ const top = entry.name;
644
+ const dirPath = root + "/" + batch + "/" + top;
645
+ refs.push({
646
+ label: top,
647
+ reference: {
648
+ source: "workbuddy",
649
+ ref: mentionFor(dirPath, true),
650
+ label: top,
651
+ appearance: "folder",
652
+ clipboardText: mentionFor(dirPath, true)
653
+ }
654
+ });
655
+ jobs.push({
656
+ kind: "dir",
657
+ entry
658
+ });
659
+ continue;
660
+ }
661
+ if (file !== null && file !== void 0) {
662
+ const rel = file.name;
686
663
  const path = root + "/" + batch + "/" + rel;
687
664
  refs.push({
688
- label: f.name,
665
+ label: file.name,
689
666
  reference: {
690
667
  source: "workbuddy",
691
668
  ref: mentionFor(path, false),
692
- label: f.name,
669
+ label: file.name,
693
670
  appearance: "file",
694
671
  clipboardText: mentionFor(path, false)
695
672
  }
696
673
  });
697
674
  jobs.push({
698
675
  kind: "file",
699
- file: f,
676
+ file,
700
677
  rel,
701
- name: f.name
678
+ name: file.name
702
679
  });
680
+ continue;
681
+ }
682
+ if (entry !== null && entry !== void 0 && entry.isFile) {
683
+ const fe = entry;
684
+ const f = await new Promise((resolve2) => fe.file((ff) => resolve2(ff), () => resolve2(null)));
685
+ if (f !== null && f !== void 0) {
686
+ const rel = f.name;
687
+ const path = root + "/" + batch + "/" + rel;
688
+ refs.push({
689
+ label: f.name,
690
+ reference: {
691
+ source: "workbuddy",
692
+ ref: mentionFor(path, false),
693
+ label: f.name,
694
+ appearance: "file",
695
+ clipboardText: mentionFor(path, false)
696
+ }
697
+ });
698
+ jobs.push({
699
+ kind: "file",
700
+ file: f,
701
+ rel,
702
+ name: f.name
703
+ });
704
+ }
703
705
  }
704
706
  }
705
- }
706
- await submitRefs(refs, jobs, batch);
707
- };
708
- const installListeners = () => {
709
- let dragDepth = 0;
710
- const onDragEnter = (e) => {
711
- if (!interceptable(e.dataTransfer)) return;
712
- e.preventDefault();
713
- e.stopPropagation();
714
- dragDepth += 1;
715
- bus.set({
716
- ...bus.get(),
717
- active: true,
718
- count: countFiles(e.dataTransfer)
719
- });
707
+ await submitRefs(refs, jobs, batch);
720
708
  };
721
- const onDragOver = (e) => {
722
- if (!interceptable(e.dataTransfer)) return;
723
- e.preventDefault();
724
- e.stopPropagation();
725
- if (!bus.get().active) {
726
- dragDepth = 1;
709
+ const installListeners = () => {
710
+ let dragDepth = 0;
711
+ const onDragEnter = (e) => {
712
+ if (!interceptable(e.dataTransfer)) return;
713
+ e.preventDefault();
714
+ e.stopPropagation();
715
+ dragDepth += 1;
727
716
  bus.set({
728
717
  ...bus.get(),
729
718
  active: true,
730
719
  count: countFiles(e.dataTransfer)
731
720
  });
732
- }
733
- };
734
- const onDragLeave = (e) => {
735
- if (!interceptable(e.dataTransfer)) return;
736
- e.preventDefault();
737
- e.stopPropagation();
738
- if (dragDepth > 0) dragDepth -= 1;
739
- if (dragDepth === 0) bus.set({
740
- ...bus.get(),
741
- active: false,
742
- count: 0
743
- });
744
- };
745
- const onDrop = (e) => {
746
- const dt = e.dataTransfer;
747
- if (dt === null || dt === void 0) return;
748
- if (!interceptable(dt)) return;
749
- e.preventDefault();
750
- e.stopPropagation();
751
- dragDepth = 0;
752
- bus.set({
753
- ...bus.get(),
754
- active: false,
755
- count: 0
756
- });
757
- const synced = syncCollect(dt);
758
- if (synced.length === 0) return;
759
- acceptAndInsert(synced, "drop-" + Date.now().toString(36)).catch((err) => {
760
- console.error("[workbuddy] drop 处理失败:", err);
761
- bus.toast("拖入处理失败:" + String(err?.message ?? err), "error");
762
- });
721
+ };
722
+ const onDragOver = (e) => {
723
+ if (!interceptable(e.dataTransfer)) return;
724
+ e.preventDefault();
725
+ e.stopPropagation();
726
+ if (!bus.get().active) {
727
+ dragDepth = 1;
728
+ bus.set({
729
+ ...bus.get(),
730
+ active: true,
731
+ count: countFiles(e.dataTransfer)
732
+ });
733
+ }
734
+ };
735
+ const onDragLeave = (e) => {
736
+ if (!interceptable(e.dataTransfer)) return;
737
+ e.preventDefault();
738
+ e.stopPropagation();
739
+ if (dragDepth > 0) dragDepth -= 1;
740
+ if (dragDepth === 0) bus.set({
741
+ ...bus.get(),
742
+ active: false,
743
+ count: 0
744
+ });
745
+ };
746
+ const onDrop = (e) => {
747
+ const dt = e.dataTransfer;
748
+ if (dt === null || dt === void 0) return;
749
+ if (!interceptable(dt)) return;
750
+ e.preventDefault();
751
+ e.stopPropagation();
752
+ dragDepth = 0;
753
+ bus.set({
754
+ ...bus.get(),
755
+ active: false,
756
+ count: 0
757
+ });
758
+ const synced = syncCollect(dt);
759
+ if (synced.length === 0) return;
760
+ acceptAndInsert(synced, "drop-" + Date.now().toString(36)).catch((err) => {
761
+ console.error("[workbuddy] drop 处理失败:", err);
762
+ bus.toast("拖入处理失败:" + String(err?.message ?? err), "error");
763
+ });
764
+ };
765
+ const onDragEnd = () => {
766
+ dragDepth = 0;
767
+ bus.set({
768
+ ...bus.get(),
769
+ active: false,
770
+ count: 0
771
+ });
772
+ };
773
+ const onPaste = (e) => {
774
+ const cd = e.clipboardData;
775
+ if (cd === null || cd === void 0 || !cd.items) return;
776
+ let hasFile = false;
777
+ for (const it of Array.from(cd.items)) if (it.kind === "file") {
778
+ hasFile = true;
779
+ break;
780
+ }
781
+ if (!hasFile) return;
782
+ e.preventDefault();
783
+ e.stopPropagation();
784
+ const synced = syncCollect(cd);
785
+ if (synced.length === 0) return;
786
+ acceptAndInsert(synced, "drop-" + Date.now().toString(36)).catch((err) => {
787
+ console.error("[workbuddy] 粘贴处理失败:", err);
788
+ bus.toast("粘贴处理失败:" + String(err?.message ?? err), "error");
789
+ });
790
+ };
791
+ window.addEventListener("dragenter", onDragEnter, true);
792
+ window.addEventListener("dragover", onDragOver, true);
793
+ window.addEventListener("dragleave", onDragLeave, true);
794
+ window.addEventListener("drop", onDrop, true);
795
+ window.addEventListener("dragend", onDragEnd, true);
796
+ window.addEventListener("paste", onPaste, true);
797
+ return () => {
798
+ window.removeEventListener("dragenter", onDragEnter, true);
799
+ window.removeEventListener("dragover", onDragOver, true);
800
+ window.removeEventListener("dragleave", onDragLeave, true);
801
+ window.removeEventListener("drop", onDrop, true);
802
+ window.removeEventListener("dragend", onDragEnd, true);
803
+ window.removeEventListener("paste", onPaste, true);
804
+ };
763
805
  };
764
- const onDragEnd = () => {
765
- dragDepth = 0;
766
- bus.set({
767
- ...bus.get(),
768
- active: false,
769
- count: 0
770
- });
806
+ return {
807
+ acceptAndInsert,
808
+ acceptTree,
809
+ installListeners
771
810
  };
772
- const onPaste = (e) => {
773
- const cd = e.clipboardData;
774
- if (cd === null || cd === void 0 || !cd.items) return;
775
- let hasFile = false;
776
- for (const it of Array.from(cd.items)) if (it.kind === "file") {
777
- hasFile = true;
778
- break;
811
+ }
812
+ //#endregion
813
+ //#region src/client/lib/insert.ts
814
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
815
+ function readCaret(fallbackLen) {
816
+ const el = document.activeElement;
817
+ if (el !== null && el !== void 0 && String(el.tagName).toUpperCase() === "TEXTAREA" && typeof el.selectionStart === "number") return el.selectionStart;
818
+ return fallbackLen;
819
+ }
820
+ function createInsertPipeline(deps) {
821
+ return async function insertItems(items) {
822
+ const sessionId = deps.sessions.list.getSnapshot().current;
823
+ if (sessionId === void 0) {
824
+ deps.toast("请先打开或新建一个会话,再拖入文件", "error");
825
+ return 0;
779
826
  }
780
- if (!hasFile) return;
781
- e.preventDefault();
782
- e.stopPropagation();
783
- const synced = syncCollect(cd);
784
- if (synced.length === 0) return;
785
- acceptAndInsert(synced, "drop-" + Date.now().toString(36)).catch((err) => {
786
- console.error("[workbuddy] 粘贴处理失败:", err);
787
- bus.toast("粘贴处理失败:" + String(err?.message ?? err), "error");
788
- });
789
- };
790
- window.addEventListener("dragenter", onDragEnter, true);
791
- window.addEventListener("dragover", onDragOver, true);
792
- window.addEventListener("dragleave", onDragLeave, true);
793
- window.addEventListener("drop", onDrop, true);
794
- window.addEventListener("dragend", onDragEnd, true);
795
- window.addEventListener("paste", onPaste, true);
796
- return () => {
797
- window.removeEventListener("dragenter", onDragEnter, true);
798
- window.removeEventListener("dragover", onDragOver, true);
799
- window.removeEventListener("dragleave", onDragLeave, true);
800
- window.removeEventListener("drop", onDrop, true);
801
- window.removeEventListener("dragend", onDragEnd, true);
802
- window.removeEventListener("paste", onPaste, true);
803
- };
804
- };
805
- return {
806
- acceptAndInsert,
807
- acceptTree,
808
- installListeners
809
- };
810
- }
811
- //#endregion
812
- //#region src/client/lib/insert.ts
813
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
814
- function readCaret(fallbackLen) {
815
- const el = document.activeElement;
816
- if (el !== null && el !== void 0 && String(el.tagName).toUpperCase() === "TEXTAREA" && typeof el.selectionStart === "number") return el.selectionStart;
817
- return fallbackLen;
818
- }
819
- function createInsertPipeline(deps) {
820
- return async function insertItems(items) {
821
- const sessionId = deps.sessions.list.getSnapshot().current;
822
- if (sessionId === void 0) {
823
- deps.toast("请先打开或新建一个会话,再拖入文件", "error");
824
- return 0;
825
- }
826
- let shell = null;
827
- try {
828
- shell = deps.conversation.input.shell(sessionId);
829
- } catch {
830
- shell = null;
831
- }
832
- let inserted = 0;
833
- let firstCaret = null;
834
- for (const item of items) if (shell !== null && shell !== void 0) {
835
- let ok = false;
836
- for (let attempt = 0; attempt < 8 && !ok; attempt += 1) {
837
- try {
838
- const st = shell.state.getSnapshot();
839
- let caret = st.draft.length;
840
- if (firstCaret === null) {
841
- firstCaret = readCaret(st.draft.length);
842
- caret = firstCaret;
827
+ let shell = null;
828
+ try {
829
+ shell = deps.conversation.input.shell(sessionId);
830
+ } catch {
831
+ shell = null;
832
+ }
833
+ let inserted = 0;
834
+ let firstCaret = null;
835
+ for (const item of items) if (shell !== null && shell !== void 0) {
836
+ let ok = false;
837
+ for (let attempt = 0; attempt < 8 && !ok; attempt += 1) {
838
+ try {
839
+ const st = shell.state.getSnapshot();
840
+ let caret = st.draft.length;
841
+ if (firstCaret === null) {
842
+ firstCaret = readCaret(st.draft.length);
843
+ caret = firstCaret;
844
+ }
845
+ const pos = Math.min(caret, st.draft.length);
846
+ ok = shell.insertReference(item.reference, {
847
+ start: pos,
848
+ end: pos,
849
+ draftRev: st.draftRev
850
+ });
851
+ } catch {
852
+ ok = false;
843
853
  }
844
- const pos = Math.min(caret, st.draft.length);
845
- ok = shell.insertReference(item.reference, {
846
- start: pos,
847
- end: pos,
848
- draftRev: st.draftRev
849
- });
850
- } catch {
851
- ok = false;
854
+ if (!ok) await sleep(90);
855
+ }
856
+ if (ok) {
857
+ inserted += 1;
858
+ continue;
852
859
  }
853
- if (!ok) await sleep(90);
854
- }
855
- if (ok) {
856
- inserted += 1;
857
- continue;
858
- }
859
- try {
860
- const st = shell.state.getSnapshot();
861
- const pos = Math.min(firstCaret !== null ? firstCaret : st.draft.length, st.draft.length);
862
- const mention = typeof item.reference.ref === "string" ? item.reference.ref : String(item.label);
863
- const next = st.draft.slice(0, pos) + mention + " " + st.draft.slice(pos);
864
- shell.setDraft(next);
865
- inserted += 1;
866
- continue;
867
- } catch (err2) {
868
860
  try {
869
- shell.notify("error", "未能插入引用「" + item.label + "」:" + String(err2?.message ?? err2));
861
+ const st = shell.state.getSnapshot();
862
+ const pos = Math.min(firstCaret !== null ? firstCaret : st.draft.length, st.draft.length);
863
+ const mention = typeof item.reference.ref === "string" ? item.reference.ref : String(item.label);
864
+ const next = st.draft.slice(0, pos) + mention + " " + st.draft.slice(pos);
865
+ shell.setDraft(next);
866
+ inserted += 1;
867
+ continue;
868
+ } catch (err2) {
869
+ try {
870
+ shell.notify("error", "未能插入引用「" + item.label + "」:" + String(err2?.message ?? err2));
871
+ } catch {}
872
+ }
873
+ } else {
874
+ const text = item.reference.ref;
875
+ const el = document.activeElement;
876
+ if (el !== null && el !== void 0 && String(el.tagName).toUpperCase() === "TEXTAREA") try {
877
+ document.execCommand("insertText", false, text + " ");
878
+ inserted += 1;
870
879
  } catch {}
880
+ else deps.toast("输入区不可用,未能插入「" + item.label + "」", "error");
871
881
  }
872
- } else {
873
- const text = item.reference.ref;
874
- const el = document.activeElement;
875
- if (el !== null && el !== void 0 && String(el.tagName).toUpperCase() === "TEXTAREA") try {
876
- document.execCommand("insertText", false, text + " ");
877
- inserted += 1;
878
- } catch {}
879
- else deps.toast("输入区不可用,未能插入「" + item.label + "」", "error");
880
- }
881
- return inserted;
882
- };
883
- }
884
- //#endregion
885
- //#region src/client/app.ts
886
- /**
887
- * Client 半侧实现(factory 模式)。
888
- *
889
- * DSH 客户端模块加载器要求 client 产物是「普通副作用脚本」:加载时调用
890
- * window.__ModuleLoader__.load({ id, factory }),factory 签名 (require) => module,
891
- * 返回 { apply, inject, name }。react 通过 factory 的 require('react') 取得
892
- * (参考 dsh-pet 的 src/client/app.ts 与 index.ts)。
893
- *
894
- * 纯逻辑模块(lib/*、at-source、definitions)不依赖 react,可安全地
895
- * 被 tsdown 内联到 bundle 顶层;组件在 factory 内以注入的 React 构造。
896
- */
897
- function makeFactory() {
898
- return function workbuddyClientFactory(require) {
899
- const React = require("react");
900
- return {
901
- name: "workbuddy-files",
902
- apply(ctx) {
903
- const get = (name) => ctx.get(name);
904
- const sessions = get("sessions");
905
- const slots = get("slots");
906
- const conversation = get("conversation");
907
- const inputTriggers = get("inputTriggers");
908
- const conversationEvents = get("conversationEvents");
909
- const styles = get("styles");
910
- if (slots === void 0) return;
911
- const insertStyles = () => styles !== void 0 ? styles.insert(CSS) : () => {};
912
- const effect = ctx.effect.bind(ctx);
913
- effect(insertStyles, "workbuddy: styles");
914
- const bus = createDropBus();
915
- let rootCache = null;
916
- dropsHome().then((r) => {
917
- rootCache = r;
918
- });
919
- const ensureRoot = async () => {
920
- if (rootCache !== null) return rootCache;
921
- const r = await dropsHome();
922
- rootCache = r;
923
- return r;
924
- };
925
- const enqueueUpload = (jobs, batch) => {
926
- runUploadJobs(jobs, batch).then(({ ok, failed }) => {
927
- if (failed.length > 0) bus.toast("缓存失败 " + failed.length + " 项:" + failed.slice(0, 2).join(";") + (failed.length > 2 ? "…" : ""), "error");
928
- else if (ok > 0) bus.toast("后台缓存完成:" + ok + " 个文件已就绪");
929
- }).catch((err) => {
930
- bus.toast("后台缓存失败:" + String(err?.message ?? err), "error");
882
+ return inserted;
883
+ };
884
+ }
885
+ //#endregion
886
+ //#region src/client/app.ts
887
+ /**
888
+ * Client 半侧实现(factory 模式)。
889
+ *
890
+ * DSH 客户端模块加载器要求 client 产物是「普通副作用脚本」:加载时调用
891
+ * window.__ModuleLoader__.load({ id, factory }),factory 签名 (require) => module,
892
+ * 返回 { apply, inject, name }。react 通过 factory 的 require('react') 取得
893
+ * (参考 dsh-pet 的 src/client/app.ts 与 index.ts)。
894
+ *
895
+ * 纯逻辑模块(lib/*、at-source、definitions)不依赖 react,可安全地
896
+ * 被 tsdown 内联到 bundle 顶层;组件在 factory 内以注入的 React 构造。
897
+ */
898
+ function makeFactory() {
899
+ return function workbuddyClientFactory(require) {
900
+ const React = require("react");
901
+ return {
902
+ name: "workbuddy-files",
903
+ apply(ctx) {
904
+ const get = (name) => ctx.get(name);
905
+ const sessions = get("sessions");
906
+ const slots = get("slots");
907
+ const conversation = get("conversation");
908
+ const inputTriggers = get("inputTriggers");
909
+ const conversationEvents = get("conversationEvents");
910
+ const styles = get("styles");
911
+ if (slots === void 0) return;
912
+ const insertStyles = () => styles !== void 0 ? styles.insert(CSS) : () => {};
913
+ const effect = ctx.effect.bind(ctx);
914
+ effect(insertStyles, "workbuddy: styles");
915
+ const bus = createDropBus();
916
+ let rootCache = null;
917
+ dropsHome().then((r) => {
918
+ rootCache = r;
931
919
  });
932
- };
933
- const handlers = createDropHandlers({
934
- bus,
935
- insert: createInsertPipeline({
936
- sessions,
937
- conversation,
938
- toast: bus.toast
939
- }),
940
- ensureRoot,
941
- enqueueUpload
942
- });
943
- effect(() => handlers.installListeners(), "workbuddy: window listeners");
944
- if (inputTriggers !== void 0) {
945
- const source = createAtSource();
946
- effect(() => inputTriggers.registerSource(source), "workbuddy: @ source");
947
- }
948
- if (conversationEvents !== void 0) effect(() => {
949
- const d1 = conversationEvents.register(boundaryDef);
950
- const d2 = conversationEvents.register(fileRefsDef);
951
- return () => {
952
- if (typeof d1 === "function") d1();
953
- if (typeof d2 === "function") d2();
920
+ const ensureRoot = async () => {
921
+ if (rootCache !== null) return rootCache;
922
+ const r = await dropsHome();
923
+ rootCache = r;
924
+ return r;
954
925
  };
955
- }, "workbuddy: conversation definitions");
956
- slots.inject("shell.overlay", () => slots.register({
957
- name: "shell.overlay",
958
- id: "workbuddy-drop",
959
- order: 300,
960
- label: "WorkBuddy 拖拽遮罩"
961
- }, createOverlayComponent(React, bus)));
962
- slots.inject("conversation.input.left", () => slots.register({
963
- name: "conversation.input.left",
964
- id: "workbuddy-pick",
965
- order: 0,
966
- label: "引用文件/文件夹"
967
- }, createPickButtonComponent(React, bus, handlers)));
968
- slots.inject("conversation.chat.turnTail", () => slots.register({
969
- name: "conversation.chat.turnTail",
970
- select: selectTurnFileRefs
971
- }, createFileCardsComponent(React)));
972
- console.log("[workbuddy-files] client 就绪:拖入即插气泡 + 后台缓存 / 统一遮罩 / 文件卡片");
973
- }
926
+ const enqueueUpload = (jobs, batch) => {
927
+ runUploadJobs(jobs, batch).then(({ ok, failed }) => {
928
+ if (failed.length > 0) bus.toast("缓存失败 " + failed.length + " 项:" + failed.slice(0, 2).join(";") + (failed.length > 2 ? "…" : ""), "error");
929
+ else if (ok > 0) bus.toast("后台缓存完成:" + ok + " 个文件已就绪");
930
+ }).catch((err) => {
931
+ bus.toast("后台缓存失败:" + String(err?.message ?? err), "error");
932
+ });
933
+ };
934
+ const handlers = createDropHandlers({
935
+ bus,
936
+ insert: createInsertPipeline({
937
+ sessions,
938
+ conversation,
939
+ toast: bus.toast
940
+ }),
941
+ ensureRoot,
942
+ enqueueUpload
943
+ });
944
+ effect(() => handlers.installListeners(), "workbuddy: window listeners");
945
+ if (inputTriggers !== void 0) {
946
+ const source = createAtSource();
947
+ effect(() => inputTriggers.registerSource(source), "workbuddy: @ source");
948
+ }
949
+ if (conversationEvents !== void 0) effect(() => {
950
+ const d1 = conversationEvents.register(boundaryDef);
951
+ const d2 = conversationEvents.register(fileRefsDef);
952
+ return () => {
953
+ if (typeof d1 === "function") d1();
954
+ if (typeof d2 === "function") d2();
955
+ };
956
+ }, "workbuddy: conversation definitions");
957
+ slots.inject("shell.overlay", () => slots.register({
958
+ name: "shell.overlay",
959
+ id: "workbuddy-drop",
960
+ order: 300,
961
+ label: "WorkBuddy 拖拽遮罩"
962
+ }, createOverlayComponent(React, bus)));
963
+ slots.inject("conversation.input.left", () => slots.register({
964
+ name: "conversation.input.left",
965
+ id: "workbuddy-pick",
966
+ order: 0,
967
+ label: "引用文件/文件夹"
968
+ }, createPickButtonComponent(React, bus, handlers)));
969
+ slots.inject("conversation.chat.turnTail", () => slots.register({
970
+ name: "conversation.chat.turnTail",
971
+ select: selectTurnFileRefs
972
+ }, createFileCardsComponent(React)));
973
+ console.log("[workbuddy-files] client 就绪:拖入即插气泡 + 后台缓存 / 统一遮罩 / 文件卡片");
974
+ }
975
+ };
974
976
  };
975
- };
976
- }
977
- //#endregion
978
- //#region src/client/index.ts
979
- window.__ModuleLoader__.load({
980
- id: "dsh-workbuddy-files",
981
- factory: makeFactory()
982
- });
983
- //#endregion
984
- export {};
977
+ }
978
+ //#endregion
979
+ //#region src/client/index.ts
980
+ window.__ModuleLoader__.load({
981
+ id: "dsh-workbuddy-files",
982
+ factory: makeFactory()
983
+ });
984
+ //#endregion
985
+ })();