@sh1robana/dsh-plugin-message-edit 0.0.0-stage → 0.3.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/client.js ADDED
@@ -0,0 +1,2775 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@sh1robana/dsh-plugin-message-edit",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
8
+ let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/shared.ts
11
+ /** Same-origin endpoint owned by the Message Edit host plugin. */
12
+ const MESSAGE_EDIT_PATH = "/api/message-edit";
13
+ //#endregion
14
+ //#region src/client/transport.ts
15
+ /** 使用页面提供的宿主传输,兼容桌面端的自定义协议和普通 Web 页面。 */
16
+ function hostFetch(input, init) {
17
+ const globals = globalThis;
18
+ const path = input.replace(/^\//, "");
19
+ const transport = globals.__DSH_TRANSPORT__;
20
+ return transport?.fetch !== void 0 ? transport.fetch(path, init) : globalThis.fetch(path, init);
21
+ }
22
+ //#endregion
23
+ //#region src/saved-user-messages.ts
24
+ function savedUserMessageSeq(event) {
25
+ if (event.type !== "user/message" || typeof event.surfaceOp !== "object" || event.surfaceOp.op !== "replace") return void 0;
26
+ const original = event.data.source.messageEdit?.originalEventSeq;
27
+ return Number.isSafeInteger(original) && original >= 0 ? original : void 0;
28
+ }
29
+ //#endregion
30
+ //#region src/build-info.ts
31
+ /** 由构建脚本生成;前后端共享同一份构建身份。 */
32
+ const MESSAGE_EDIT_BUILD_INFO = Object.freeze({
33
+ "version": "0.3.0",
34
+ "buildId": "952c32134401a88f10f7",
35
+ "targetDshVersion": "0.2.0-rc.2"
36
+ });
37
+ //#endregion
38
+ //#region src/client/attachmentTools.ts
39
+ function attachmentTools(ctx, sessionId) {
40
+ const conversation = () => {
41
+ const service = ctx.get("conversation");
42
+ if (service === void 0) throw new Error("DSH 附件服务尚未就绪,请重启后重试。");
43
+ return service;
44
+ };
45
+ return {
46
+ create: (files) => conversation().createDrafts(sessionId, files),
47
+ serialize: async (ids) => (await conversation().serializeDraftAttachments(ids)).attachments,
48
+ release: (id) => {
49
+ conversation().releaseDraftAttachment(id);
50
+ },
51
+ retry: (id) => {
52
+ conversation().retryFileUpload(sessionId, id);
53
+ },
54
+ uploads: () => conversation().fileUploads.getSnapshot(),
55
+ subscribe: (listener) => conversation().fileUploads.subscribe(listener),
56
+ imageUrl: (attachment) => ctx.uiConversation.imageUrl(sessionId, attachment)
57
+ };
58
+ }
59
+ //#endregion
60
+ //#region src/client/composerTools.ts
61
+ async function readResponse(response) {
62
+ const value = await response.json();
63
+ if (!response.ok) throw new Error(value.error ?? `请求失败:HTTP ${String(response.status)}`);
64
+ return value;
65
+ }
66
+ function composerTools(ctx, sessionId) {
67
+ const get = async (query, signal) => {
68
+ return readResponse(await hostFetch(`${MESSAGE_EDIT_PATH}?${new URLSearchParams({
69
+ sessionId,
70
+ ...query
71
+ })}`, {
72
+ method: "GET",
73
+ cache: "no-store",
74
+ ...signal === void 0 ? {} : { signal }
75
+ }));
76
+ };
77
+ const post = async (operation) => {
78
+ await readResponse(await hostFetch(MESSAGE_EDIT_PATH, {
79
+ method: "POST",
80
+ headers: { "content-type": "application/json" },
81
+ body: JSON.stringify({
82
+ sessionId,
83
+ ...operation
84
+ })
85
+ }));
86
+ };
87
+ return {
88
+ options: async () => await get({ view: "options" }),
89
+ references: async (query, signal) => {
90
+ const remote = ctx.get("remote");
91
+ const [files, sessions] = await Promise.all([get({
92
+ view: "references",
93
+ query
94
+ }, signal), remote?.sessionReferenceResolver?.candidates(sessionId, query, signal)]);
95
+ return [...files, ...sessions?.ok ? sessions.value.map((item) => ({
96
+ ...item,
97
+ kind: "session"
98
+ })) : []];
99
+ },
100
+ openPath: (path) => post({
101
+ action: "open-reference",
102
+ path
103
+ }),
104
+ openSession: (id) => {
105
+ ctx.uiWorkspace.openSession(id);
106
+ },
107
+ openAttachment: (eventSeq, blockIndex) => post({
108
+ action: "open-attachment",
109
+ eventSeq,
110
+ blockIndex
111
+ }),
112
+ openUpload: (receiptId) => post({
113
+ action: "open-upload",
114
+ receiptId
115
+ }),
116
+ commands: async (query, signal) => {
117
+ const commands = ctx.get("commandUi");
118
+ return commands === void 0 ? [] : commands.candidates({ sessionId }, {
119
+ query,
120
+ position: "leading",
121
+ signal
122
+ });
123
+ },
124
+ command: async (line) => {
125
+ const commands = ctx.get("commandUi");
126
+ if (commands !== void 0) {
127
+ const result = await commands.execute({ sessionId }, line);
128
+ if (result.kind === "error") throw new Error(result.text);
129
+ return;
130
+ }
131
+ const session = ctx.get("sessions").binding(sessionId)?.session;
132
+ if (session === void 0) throw new Error("当前会话尚未就绪,请稍后重试。");
133
+ const result = await session.command(line);
134
+ if (!result.ok) throw new Error(result.error.message);
135
+ if (!result.value.matched) throw new Error("当前 DSH 未提供此指令。");
136
+ }
137
+ };
138
+ }
139
+ //#endregion
140
+ //#region src/client/controller.ts
141
+ /** 合并回合完成时的后台刷新,不延迟首次加载。 */
142
+ const REFRESH_DELAY_MS = 300;
143
+ function messageOf(error) {
144
+ return error instanceof Error ? error.message : String(error);
145
+ }
146
+ function objectValue(value, label) {
147
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${label} 不是对象`);
148
+ return value;
149
+ }
150
+ function stringValue(value, label) {
151
+ if (typeof value !== "string") throw new TypeError(`${label} 不是字符串`);
152
+ return value;
153
+ }
154
+ function numberValue(value, label) {
155
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${label} 不是数字`);
156
+ return value;
157
+ }
158
+ function booleanValue(value, label) {
159
+ if (typeof value !== "boolean") throw new TypeError(`${label} 不是布尔值`);
160
+ return value;
161
+ }
162
+ function blockKind(value) {
163
+ if (value !== "user" && value !== "assistant.reasoning" && value !== "assistant.response") throw new TypeError("消息块类型无效");
164
+ return value;
165
+ }
166
+ function decodeMessage(value, index) {
167
+ const row = objectValue(value, `messages[${String(index)}]`);
168
+ return {
169
+ key: stringValue(row["key"], "消息 key"),
170
+ turn: numberValue(row["turn"], "消息 turn"),
171
+ eventSeq: numberValue(row["eventSeq"], "消息 eventSeq"),
172
+ blockIndex: numberValue(row["blockIndex"], "消息 blockIndex"),
173
+ kind: blockKind(row["kind"]),
174
+ text: stringValue(row["text"], "消息 text"),
175
+ time: numberValue(row["time"], "消息 time"),
176
+ ...row["content"] === void 0 ? {} : { content: arrayValue(row["content"], "消息 content") },
177
+ ...row["attachments"] === void 0 ? {} : { attachments: arrayValue(row["attachments"], "消息 attachments") }
178
+ };
179
+ }
180
+ function decodeRetryable(value, index) {
181
+ const row = objectValue(value, `retryableTurns[${String(index)}]`);
182
+ return {
183
+ turn: numberValue(row["turn"], "回合 turn"),
184
+ userEventSeq: numberValue(row["userEventSeq"], "回合 userEventSeq"),
185
+ preview: stringValue(row["preview"], "回合 preview"),
186
+ time: numberValue(row["time"], "回合 time")
187
+ };
188
+ }
189
+ function optionalOperation(value) {
190
+ if (value === void 0) return void 0;
191
+ if (value === "edit" || value === "reroll" || value === "retry") return value;
192
+ throw new TypeError("版本 operation 无效");
193
+ }
194
+ function decodeVersion(value, index) {
195
+ const row = objectValue(value, `versions[${String(index)}]`);
196
+ const operation = optionalOperation(row["operation"]);
197
+ const cascade = row["cascade"];
198
+ if (cascade !== void 0 && cascade !== "truncate" && cascade !== "preserve") throw new TypeError("版本 cascade 无效");
199
+ const kind = row["blockKind"] === void 0 ? void 0 : blockKind(row["blockKind"]);
200
+ return {
201
+ sessionId: stringValue(row["sessionId"], "版本 sessionId"),
202
+ ...row["parentSessionId"] === void 0 ? {} : { parentSessionId: stringValue(row["parentSessionId"], "版本 parentSessionId") },
203
+ ...row["effectId"] === void 0 ? {} : { effectId: stringValue(row["effectId"], "版本 effectId") },
204
+ ...row["inverseSessionId"] === void 0 ? {} : { inverseSessionId: stringValue(row["inverseSessionId"], "版本 inverseSessionId") },
205
+ createdAt: numberValue(row["createdAt"], "版本 createdAt"),
206
+ depth: numberValue(row["depth"], "版本 depth"),
207
+ current: booleanValue(row["current"], "版本 current"),
208
+ onCurrentEffectPath: booleanValue(row["onCurrentEffectPath"], "版本 onCurrentEffectPath"),
209
+ ...operation === void 0 ? {} : { operation },
210
+ ...cascade === void 0 ? {} : { cascade },
211
+ ...row["targetTurn"] === void 0 ? {} : { targetTurn: numberValue(row["targetTurn"], "版本 targetTurn") },
212
+ ...kind === void 0 ? {} : { blockKind: kind },
213
+ ...row["before"] === void 0 ? {} : { before: stringValue(row["before"], "版本 before") },
214
+ ...row["after"] === void 0 ? {} : { after: stringValue(row["after"], "版本 after") }
215
+ };
216
+ }
217
+ function arrayValue(value, label) {
218
+ if (!Array.isArray(value)) throw new TypeError(`${label} 不是数组`);
219
+ return value;
220
+ }
221
+ function stringArray(value, label) {
222
+ return arrayValue(value, label).map((item, index) => stringValue(item, `${label}[${String(index)}]`));
223
+ }
224
+ function decodeTimeline(value) {
225
+ const data = objectValue(value, "Timeline 响应");
226
+ return {
227
+ sessionId: stringValue(data["sessionId"], "Timeline sessionId"),
228
+ messages: arrayValue(data["messages"], "Timeline messages").map(decodeMessage),
229
+ retryableTurns: arrayValue(data["retryableTurns"], "Timeline retryableTurns").map(decodeRetryable),
230
+ versions: arrayValue(data["versions"], "Timeline versions").map(decodeVersion),
231
+ undoStack: stringArray(data["undoStack"], "Timeline undoStack"),
232
+ redoSessionIds: stringArray(data["redoSessionIds"], "Timeline redoSessionIds")
233
+ };
234
+ }
235
+ function decodeOperationResult(value) {
236
+ const data = objectValue(value, "操作响应");
237
+ return {
238
+ sessionId: stringValue(data["sessionId"], "操作 sessionId"),
239
+ queuedTurns: numberValue(data["queuedTurns"], "操作 queuedTurns"),
240
+ ...data["saved"] === void 0 ? {} : { saved: booleanValue(data["saved"], "操作 saved") },
241
+ ...data["unchanged"] === void 0 ? {} : { unchanged: booleanValue(data["unchanged"], "操作 unchanged") }
242
+ };
243
+ }
244
+ async function responseValue(response) {
245
+ const value = await response.json();
246
+ if (response.ok) return value;
247
+ const error = objectValue(value, "错误响应")["error"];
248
+ throw new Error(typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`);
249
+ }
250
+ function conversationRevision(snapshot) {
251
+ const changes = snapshot.entries.flatMap((entry) => {
252
+ if (entry.type !== "event") return [];
253
+ if (entry.event.type === "turn/end") return [`turn:${String(entry.event.data.turn)}:${String(entry.event.seq)}`];
254
+ if (savedUserMessageSeq(entry.event) !== void 0) return [`save:${String(entry.event.seq)}`];
255
+ return [];
256
+ });
257
+ return [snapshot.hasMore, changes.join(",")].join("|");
258
+ }
259
+ function lineageRevision(snapshot, sessionId) {
260
+ let root = sessionId;
261
+ const ancestorIds = /* @__PURE__ */ new Set();
262
+ while (!ancestorIds.has(root)) {
263
+ ancestorIds.add(root);
264
+ const parent = snapshot.byId[root]?.parentId;
265
+ if (parent === void 0 || snapshot.byId[parent] === void 0) break;
266
+ root = parent;
267
+ }
268
+ const connected = [];
269
+ for (const rawId of Object.keys(snapshot.byId).sort()) {
270
+ const id = rawId;
271
+ const seen = /* @__PURE__ */ new Set();
272
+ let cursor = id;
273
+ while (cursor !== void 0 && !seen.has(cursor)) {
274
+ if (cursor === root) {
275
+ connected.push(`${id}>${snapshot.byId[id]?.parentId ?? ""}`);
276
+ break;
277
+ }
278
+ seen.add(cursor);
279
+ cursor = snapshot.byId[cursor]?.parentId;
280
+ }
281
+ }
282
+ return connected.join("|");
283
+ }
284
+ /** One stable controller is shared by all entries mounted for the same session. */
285
+ var MessageEditController = class {
286
+ sessionId;
287
+ store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
288
+ status: "idle",
289
+ error: null,
290
+ pending: null,
291
+ timeline: null
292
+ });
293
+ face;
294
+ generation = 0;
295
+ ctx;
296
+ sessions;
297
+ sessionSource;
298
+ sessionSourceDispose;
299
+ sessionRevision;
300
+ listRevision = "";
301
+ refreshScheduled = false;
302
+ refreshTimer;
303
+ observing = false;
304
+ disposeObservation = void 0;
305
+ inflight = null;
306
+ rerunAfter = false;
307
+ abort = null;
308
+ disposed = false;
309
+ users = 0;
310
+ constructor(ctx, sessionId) {
311
+ this.sessionId = sessionId;
312
+ this.ctx = ctx;
313
+ this.sessions = ctx.get("sessions");
314
+ this.face = {
315
+ hooks: { messageEdit: this.store },
316
+ acquire: () => {
317
+ this.users += 1;
318
+ if (this.users === 1 && this.disposed) this.revive();
319
+ return () => this.release();
320
+ },
321
+ load: () => {
322
+ const status = this.store.getSnapshot().status;
323
+ if (status === "idle" || status === "error") this.load();
324
+ },
325
+ attachmentTools: attachmentTools(ctx, this.sessionId),
326
+ composerTools: composerTools(ctx, this.sessionId),
327
+ edit: (message, text, cascade, regenerate = true, attachments, settings) => this.mutate(message.kind === "user" && !regenerate ? {
328
+ action: "save",
329
+ sessionId: this.sessionId,
330
+ eventSeq: message.eventSeq,
331
+ blockIndex: message.blockIndex,
332
+ text,
333
+ ...attachments === void 0 ? {} : { attachments },
334
+ ...settings === void 0 ? {} : { settings }
335
+ } : {
336
+ action: "edit",
337
+ sessionId: this.sessionId,
338
+ eventSeq: message.eventSeq,
339
+ blockIndex: message.blockIndex,
340
+ text,
341
+ cascade,
342
+ regenerate,
343
+ ...attachments === void 0 ? {} : { attachments },
344
+ ...settings === void 0 ? {} : { settings }
345
+ }),
346
+ retry: (turn, cascade) => this.mutate({
347
+ action: "retry",
348
+ sessionId: this.sessionId,
349
+ turn,
350
+ cascade
351
+ }),
352
+ reroll: () => this.mutate({
353
+ action: "reroll",
354
+ sessionId: this.sessionId
355
+ }),
356
+ openVersion: (sessionId) => this.openWhenListed(sessionId)
357
+ };
358
+ this.observe();
359
+ }
360
+ observe() {
361
+ this.disposeObservation = this.ctx.effect(() => this.observeDependencies(), `message-edit: observe ${this.sessionId}`);
362
+ }
363
+ release() {
364
+ this.users -= 1;
365
+ if (this.users <= 0) this.dispose();
366
+ }
367
+ /** Tear subscriptions down once no mounted entry uses this controller. */
368
+ dispose() {
369
+ if (this.disposed) return;
370
+ this.disposed = true;
371
+ this.generation += 1;
372
+ if (this.refreshTimer !== void 0) {
373
+ clearTimeout(this.refreshTimer);
374
+ this.refreshTimer = void 0;
375
+ this.refreshScheduled = false;
376
+ }
377
+ this.abort?.abort();
378
+ this.abort = null;
379
+ this.disposeObservation?.();
380
+ this.disposeObservation = void 0;
381
+ }
382
+ /** Re-observe after a transient zero; the retained store keeps old data
383
+ * until the immediate refetch below commits. */
384
+ revive() {
385
+ this.disposed = false;
386
+ this.observe();
387
+ this.refresh();
388
+ }
389
+ /** Bind to replaceable value sources instead of retaining a Session object. */
390
+ observeDependencies() {
391
+ this.observing = true;
392
+ this.listRevision = lineageRevision(this.sessions.list.getSnapshot(), this.sessionId);
393
+ this.bindSessionSource();
394
+ const disposeList = this.sessions.list.subscribe(() => {
395
+ const rebound = this.bindSessionSource();
396
+ const nextRevision = lineageRevision(this.sessions.list.getSnapshot(), this.sessionId);
397
+ if (nextRevision === this.listRevision && !rebound) return;
398
+ this.listRevision = nextRevision;
399
+ this.invalidate();
400
+ });
401
+ return () => {
402
+ this.observing = false;
403
+ this.generation += 1;
404
+ disposeList();
405
+ this.sessionSourceDispose?.();
406
+ this.sessionSourceDispose = void 0;
407
+ this.sessionSource = void 0;
408
+ this.sessionRevision = void 0;
409
+ };
410
+ }
411
+ bindSessionSource() {
412
+ const source = this.sessions.binding(this.sessionId)?.eventSource;
413
+ if (source === this.sessionSource) return false;
414
+ this.sessionSourceDispose?.();
415
+ this.sessionSource = source;
416
+ this.sessionRevision = source === void 0 ? void 0 : conversationRevision(source.getSnapshot());
417
+ this.sessionSourceDispose = source?.subscribe(() => {
418
+ if (this.sessionSource !== source) return;
419
+ const revision = conversationRevision(source.getSnapshot());
420
+ if (revision === this.sessionRevision) return;
421
+ this.sessionRevision = revision;
422
+ this.invalidate();
423
+ });
424
+ return true;
425
+ }
426
+ invalidate() {
427
+ if (!this.observing || this.store.getSnapshot().status === "idle" || this.refreshScheduled) return;
428
+ this.refreshScheduled = true;
429
+ this.refreshTimer = setTimeout(() => {
430
+ this.refreshTimer = void 0;
431
+ this.refreshScheduled = false;
432
+ if (this.observing && this.store.getSnapshot().status !== "idle") this.refresh();
433
+ }, REFRESH_DELAY_MS);
434
+ }
435
+ /** Invalidation-driven refetch: one in-flight request absorbs the demand
436
+ * and commits a single rerun once it settles. */
437
+ refresh() {
438
+ if (this.disposed) return;
439
+ if (this.inflight !== null) {
440
+ this.rerunAfter = true;
441
+ return;
442
+ }
443
+ this.load();
444
+ }
445
+ /** Refetch the full value-level projection; concurrent callers share one
446
+ * request, and an invalidation during flight schedules exactly one rerun. */
447
+ async load() {
448
+ if (this.disposed) return;
449
+ if (this.inflight !== null) return this.inflight;
450
+ const generation = ++this.generation;
451
+ this.abort?.abort();
452
+ const abort = new AbortController();
453
+ this.abort = abort;
454
+ this.store.update((state) => {
455
+ if (state.status !== "ready") state.status = "loading";
456
+ state.error = null;
457
+ });
458
+ const run = this.performLoad(generation, abort);
459
+ this.inflight = run;
460
+ try {
461
+ await run;
462
+ } finally {
463
+ if (this.inflight === run) this.inflight = null;
464
+ if (this.rerunAfter && !this.disposed) {
465
+ this.rerunAfter = false;
466
+ this.load();
467
+ }
468
+ }
469
+ }
470
+ async performLoad(generation, abort) {
471
+ try {
472
+ const value = await responseValue(await hostFetch(`${MESSAGE_EDIT_PATH}?sessionId=${encodeURIComponent(this.sessionId)}`, {
473
+ method: "GET",
474
+ headers: { accept: "application/json" },
475
+ cache: "no-store",
476
+ signal: abort.signal
477
+ }));
478
+ const build = objectValue(value, "Timeline 响应")["build"];
479
+ if (build !== void 0) {
480
+ const identity = objectValue(build, "宿主构建");
481
+ if (identity["version"] !== MESSAGE_EDIT_BUILD_INFO.version || identity["buildId"] !== MESSAGE_EDIT_BUILD_INFO.buildId) throw new Error("消息编辑插件的前后端版本不一致,请彻底退出 DSH 后重新启动;浏览器页面请刷新。");
482
+ }
483
+ const timeline = decodeTimeline(value);
484
+ if (generation !== this.generation) return;
485
+ this.store.update((state) => {
486
+ state.status = "ready";
487
+ state.error = null;
488
+ state.timeline = timeline;
489
+ });
490
+ } catch (error) {
491
+ if (generation !== this.generation) return;
492
+ this.store.update((state) => {
493
+ state.status = "error";
494
+ state.error = messageOf(error);
495
+ });
496
+ }
497
+ }
498
+ /** Refresh only controllers whose projection has already been requested. */
499
+ refreshIfLoaded() {
500
+ if (this.disposed || this.store.getSnapshot().status === "idle") return;
501
+ this.refresh();
502
+ }
503
+ async mutate(operation) {
504
+ const current = this.store.getSnapshot();
505
+ if (current.pending !== null || current.status !== "ready") return false;
506
+ this.store.update((state) => {
507
+ state.pending = operation.action === "save" ? "edit" : operation.action;
508
+ state.error = null;
509
+ });
510
+ try {
511
+ const result = decodeOperationResult(await responseValue(await hostFetch(MESSAGE_EDIT_PATH, {
512
+ method: "POST",
513
+ headers: {
514
+ accept: "application/json",
515
+ "content-type": "application/json"
516
+ },
517
+ body: JSON.stringify(operation)
518
+ })));
519
+ if (result.unchanged === true && (result.sessionId !== this.sessionId || result.queuedTurns !== 0)) throw new Error("宿主未确认无修改保存,请更新插件并重启 DSH。");
520
+ if (operation.action === "save" && (result.sessionId !== this.sessionId || result.queuedTurns !== 0 || result.saved !== true)) throw new Error("宿主未确认在当前会话保存,请更新插件并彻底重启 DSH。");
521
+ if (!this.disposed && result.unchanged !== true) if (operation.action === "save") {
522
+ if (this.inflight !== null) {
523
+ this.rerunAfter = true;
524
+ await this.load();
525
+ }
526
+ await this.load();
527
+ } else await this.openWhenListed(result.sessionId);
528
+ this.store.update((state) => {
529
+ state.pending = null;
530
+ });
531
+ return true;
532
+ } catch (error) {
533
+ this.store.update((state) => {
534
+ state.pending = null;
535
+ if (!this.disposed) state.error = messageOf(error);
536
+ });
537
+ return false;
538
+ }
539
+ }
540
+ /** 通过新版工作区导航持有会话引用,并同步切换主视图。 */
541
+ async openWhenListed(sessionId) {
542
+ if (this.disposed) return;
543
+ await this.sessions.refresh();
544
+ if (!this.disposed) this.ctx.uiWorkspace.openSession(sessionId);
545
+ }
546
+ };
547
+ //#endregion
548
+ //#region \0dsh-css:D:\ai\home\codex\dsh-message-edit\src\client\InlineMessageEdit.module.css.mjs
549
+ const css$2 = ".jW03kW_overlay,.jW03kW_composer{--message-edit-panel:#fafbfc;--message-edit-hover:#eef0f3;--message-edit-border:#dce0e7;--message-edit-text:#1d2430;--message-edit-secondary:#687487;--message-edit-danger:#c62828;--message-edit-link:#406bf0;color:var(--message-edit-text);color-scheme:light}body[data-ds-dark-theme] .jW03kW_overlay,body[data-ds-dark-theme] .jW03kW_composer{--message-edit-panel:#25272b;--message-edit-hover:#33363d;--message-edit-border:#494d56;--message-edit-text:#eef0f5;--message-edit-secondary:#b0b7c4;--message-edit-danger:#ff8e8e;--message-edit-link:#8da9ff;color-scheme:dark}.jW03kW_overlay{z-index:1000;background:var(--dsw-alias-bg-mask,#00000073);box-sizing:border-box;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}.jW03kW_panel{box-sizing:border-box;border:1px solid var(--message-edit-border);background:var(--message-edit-panel);border-radius:10px;width:min(960px,100%);max-height:calc(100dvh - 48px);padding:20px 24px;overflow:auto}.jW03kW_title{color:var(--message-edit-text);padding:4px 0 10px;font-size:15px}.jW03kW_input{box-sizing:border-box;border:1px solid var(--message-edit-border);background:var(--message-edit-panel);width:100%;height:min(52dvh,560px);min-height:min(320px,40dvh);max-height:calc(100dvh - 180px);color:var(--message-edit-text);font:inherit;resize:vertical;border-radius:8px;padding:10px;line-height:1.6}.jW03kW_footer{flex-wrap:wrap;justify-content:flex-end;gap:8px;padding:10px 0 0;display:flex}.jW03kW_footer button{border:1px solid var(--message-edit-border);background:var(--message-edit-hover);color:var(--message-edit-text);cursor:pointer;border-radius:6px;padding:6px 14px}.jW03kW_footer button:disabled{cursor:wait;opacity:.6}.jW03kW_attachmentEditor{padding:0 0 10px}.jW03kW_attachmentList{flex-wrap:wrap;gap:10px;max-height:180px;display:flex;overflow:auto}.jW03kW_attachmentList:empty{display:none}.jW03kW_attachmentCard{box-sizing:border-box;border:1px solid var(--message-edit-border);width:240px;min-height:72px;color:var(--message-edit-text);background:var(--message-edit-panel);border-radius:8px;align-items:center;gap:10px;padding:8px 30px 8px 8px;display:flex;position:relative}.jW03kW_attachmentPreview{object-fit:contain;width:64px;height:64px;color:var(--message-edit-secondary);background:var(--message-edit-hover);border-radius:6px;flex:0 0 64px;justify-content:center;align-items:center;font-size:12px;display:flex}.jW03kW_attachmentInfo{flex-direction:column;gap:4px;min-width:0;font-size:13px;display:flex}.jW03kW_attachmentInfo>span:first-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.jW03kW_attachmentStatus{color:var(--message-edit-secondary);overflow-wrap:anywhere;font-size:11px}.jW03kW_attachmentRemove{background:var(--message-edit-hover);width:22px;height:22px;color:var(--message-edit-text);cursor:pointer;border:none;border-radius:50%;padding:0;font-size:18px;position:absolute;top:4px;right:4px}.jW03kW_attachmentRemove:disabled{opacity:.5;cursor:wait}.jW03kW_attachmentAdd{border:1px solid var(--message-edit-border);background:var(--message-edit-hover);color:var(--message-edit-text);cursor:pointer;border-radius:6px;margin:8px 0 0;padding:6px 10px}.jW03kW_footer .jW03kW_attachmentAdd{margin:0 auto 0 0}.jW03kW_attachmentNotice{color:var(--message-edit-danger);padding-top:4px;font-size:12px}.jW03kW_attachmentNotice:empty{display:none}.jW03kW_confirmationOverlay{z-index:1001}.jW03kW_confirmPanel{box-sizing:border-box;border:1px solid var(--message-edit-border);background:var(--message-edit-panel);border-radius:10px;width:min(400px,100%);padding:20px 24px}@media (width<=600px){.jW03kW_overlay{padding:12px}.jW03kW_panel{max-height:calc(100dvh - 24px);padding:16px}}.jW03kW_iconButton{width:20px;height:20px;color:var(--message-edit-secondary);cursor:pointer;background:0 0;border:none;border-radius:4px;justify-content:center;align-items:center;padding:2px;display:inline-flex}.jW03kW_iconButton:hover{color:var(--message-edit-text);background:var(--message-edit-hover)}.jW03kW_picker{flex-direction:column;gap:6px;padding:4px 0 12px;display:flex}.jW03kW_pickerItem{border:1px solid var(--message-edit-border);background:var(--message-edit-hover);color:var(--message-edit-text);text-align:left;cursor:pointer;border-radius:6px;padding:8px 10px;font-size:12px}.jW03kW_pickerItem:hover{background:var(--message-edit-panel)}.jW03kW_pickerItemActive{border:1px solid var(--message-edit-border);background:var(--message-edit-hover);color:var(--message-edit-text);cursor:pointer;border-radius:6px;align-self:flex-end;padding:6px 14px}.jW03kW_composer{border:1px solid var(--message-edit-border);background:var(--message-edit-panel);border-radius:20px;padding:14px 16px 10px;position:relative}.jW03kW_composer:focus-within{border-color:var(--dsw-alias-brand-primary,#4d77f5)}.jW03kW_richInput{white-space:pre-wrap;overflow-wrap:anywhere;resize:none;border:none;border-radius:0;outline:none;height:min(42dvh,500px);min-height:min(250px,32dvh);padding:8px 0;overflow:auto}.jW03kW_richInput:empty:before{content:attr(data-placeholder);color:var(--message-edit-secondary);pointer-events:none}.jW03kW_fileReference{color:var(--message-edit-link);cursor:pointer;white-space:pre-wrap;border-radius:4px;padding:0 2px;display:inline}.jW03kW_fileReference:hover{background:var(--message-edit-hover)}.jW03kW_composerToolbar{flex-wrap:wrap;align-items:center;gap:8px;padding-top:8px;display:flex}.jW03kW_composerToolbar button{color:var(--message-edit-secondary);font:inherit;cursor:pointer;background:0 0;border:none}.jW03kW_composerToolbar button:hover{background:var(--message-edit-hover)}.jW03kW_composerToolbar .jW03kW_composerPlus{background:var(--message-edit-hover);border-radius:50%;width:34px;height:34px;padding:0;font-size:24px;line-height:1}.jW03kW_composerControl,.jW03kW_composerModel{border-radius:8px;padding:6px 8px;font-size:13px}.jW03kW_composerModel{overflow-wrap:anywhere;max-width:70%;margin-left:auto}.jW03kW_composerMenu,.jW03kW_referenceSuggestions{z-index:2;border:1px solid var(--message-edit-border);background:var(--message-edit-panel);border-radius:14px;max-height:min(44dvh,400px);padding:8px;position:absolute;bottom:52px;left:12px;right:12px;overflow:auto;box-shadow:0 4px 20px #0001}.jW03kW_composerMenu[hidden],.jW03kW_referenceSuggestions[hidden]{display:none}.jW03kW_composerMenu button,.jW03kW_referenceSuggestions button{width:100%;color:var(--message-edit-text);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:8px;justify-content:space-between;align-items:center;gap:16px;padding:9px 10px;font-size:13px;display:flex}.jW03kW_composerMenu button:hover,.jW03kW_referenceSuggestions button:hover,.jW03kW_referenceSuggestions button[aria-selected=true]{background:var(--message-edit-hover)}.jW03kW_composerMenu .jW03kW_menuValue{color:var(--message-edit-secondary);text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:12px;overflow:hidden}.jW03kW_composerMenu[data-kind=permission],.jW03kW_composerMenu[data-kind=model],.jW03kW_composerMenu[data-kind=modelList]{box-sizing:border-box;border-radius:16px;width:min(320px,100% - 32px);padding:6px;left:auto;right:16px}.jW03kW_composerMenu[data-kind=permission]{width:min(220px,100% - 70px);left:54px;right:auto}.jW03kW_composerMenu[data-kind=modelList]{max-height:none;overflow:hidden}.jW03kW_composerMenu[data-kind=permission] button,.jW03kW_composerMenu[data-kind=model] button,.jW03kW_composerMenu[data-kind=modelList] button{white-space:nowrap;min-height:38px;padding:8px 10px;font-size:14px;overflow:hidden}.jW03kW_composerMenu button[role=menuitemradio] .jW03kW_menuValue{color:var(--message-edit-text);text-align:right;flex:0 0 16px}.jW03kW_composerMenu .jW03kW_menuBack{min-height:28px;color:var(--message-edit-secondary);font-size:12px}.jW03kW_menuItemLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.jW03kW_composerMenu button[role=menuitemradio] .jW03kW_menuItemLabel{flex:1}.jW03kW_composerMenu .jW03kW_modelSearch{border:none;outline:none;padding:8px 10px;font-size:14px}.jW03kW_modelSearch:focus-visible{box-shadow:inset 0 0 0 1px var(--message-edit-border)}.jW03kW_modelList{max-height:min(38dvh,280px);overflow-y:auto}.jW03kW_composerMenu input{box-sizing:border-box;border:1px solid var(--message-edit-border);background:inherit;width:100%;color:var(--message-edit-text);font:inherit;border-radius:8px;padding:10px}.jW03kW_menuHeading{color:var(--message-edit-secondary);padding:8px 10px 4px;font-size:12px}.jW03kW_attachmentAdd.jW03kW_attachmentAddCircle{border-radius:50%;width:34px;height:34px;padding:0;font-size:24px;line-height:1}.jW03kW_attachmentOpen{width:100%;min-width:0;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:10px;padding:0;display:flex}.jW03kW_attachmentOpen[data-kind=image]{cursor:zoom-in}.jW03kW_attachmentOpen:disabled{cursor:wait}.jW03kW_attachmentOpen:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d77f5);outline-offset:3px;border-radius:6px}.jW03kW_attachmentLightbox{z-index:1100;box-sizing:border-box;background:#000000d6;justify-content:center;align-items:center;padding:40px;display:flex;position:fixed;inset:0}.jW03kW_attachmentOriginal{object-fit:contain;width:auto;max-width:100%;height:auto;max-height:100%;display:block}.jW03kW_attachmentPreviewClose{color:#fff;cursor:pointer;background:#ffffff29;border:none;border-radius:50%;justify-content:center;align-items:center;width:36px;height:36px;padding:0;font-size:28px;display:flex;position:absolute;top:14px;right:18px}.jW03kW_attachmentPreviewClose:focus-visible{outline-offset:3px;outline:2px solid #fff}";
550
+ const tagId$2 = "@sh1robana/dsh-plugin-message-edit/InlineMessageEdit.module.css";
551
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
552
+ const tag = document.createElement("style");
553
+ tag.dataset.plugin = "@sh1robana/dsh-plugin-message-edit";
554
+ tag.dataset.pluginCss = tagId$2;
555
+ tag.textContent = css$2;
556
+ document.head.appendChild(tag);
557
+ }
558
+ var InlineMessageEdit_module_css_default = {
559
+ "input": "jW03kW_input",
560
+ "attachmentInfo": "jW03kW_attachmentInfo",
561
+ "attachmentStatus": "jW03kW_attachmentStatus",
562
+ "menuBack": "jW03kW_menuBack",
563
+ "attachmentOpen": "jW03kW_attachmentOpen",
564
+ "composerPlus": "jW03kW_composerPlus",
565
+ "attachmentOriginal": "jW03kW_attachmentOriginal",
566
+ "title": "jW03kW_title",
567
+ "menuItemLabel": "jW03kW_menuItemLabel",
568
+ "attachmentCard": "jW03kW_attachmentCard",
569
+ "pickerItemActive": "jW03kW_pickerItemActive",
570
+ "attachmentAddCircle": "jW03kW_attachmentAddCircle",
571
+ "attachmentLightbox": "jW03kW_attachmentLightbox",
572
+ "picker": "jW03kW_picker",
573
+ "attachmentPreview": "jW03kW_attachmentPreview",
574
+ "menuValue": "jW03kW_menuValue",
575
+ "referenceSuggestions": "jW03kW_referenceSuggestions",
576
+ "attachmentRemove": "jW03kW_attachmentRemove",
577
+ "overlay": "jW03kW_overlay",
578
+ "pickerItem": "jW03kW_pickerItem",
579
+ "footer": "jW03kW_footer",
580
+ "confirmPanel": "jW03kW_confirmPanel",
581
+ "composer": "jW03kW_composer",
582
+ "modelSearch": "jW03kW_modelSearch",
583
+ "attachmentEditor": "jW03kW_attachmentEditor",
584
+ "fileReference": "jW03kW_fileReference",
585
+ "attachmentNotice": "jW03kW_attachmentNotice",
586
+ "menuHeading": "jW03kW_menuHeading",
587
+ "panel": "jW03kW_panel",
588
+ "iconButton": "jW03kW_iconButton",
589
+ "attachmentPreviewClose": "jW03kW_attachmentPreviewClose",
590
+ "confirmationOverlay": "jW03kW_confirmationOverlay",
591
+ "composerMenu": "jW03kW_composerMenu",
592
+ "attachmentList": "jW03kW_attachmentList",
593
+ "composerControl": "jW03kW_composerControl",
594
+ "modelList": "jW03kW_modelList",
595
+ "richInput": "jW03kW_richInput",
596
+ "composerToolbar": "jW03kW_composerToolbar",
597
+ "composerModel": "jW03kW_composerModel",
598
+ "attachmentAdd": "jW03kW_attachmentAdd"
599
+ };
600
+ //#endregion
601
+ //#region src/client/confirmCancelEdit.ts
602
+ /** 确认取消时保留原编辑器,只有选择“是”才提交关闭操作。 */
603
+ function confirmCancelEdit(onConfirm, onReject) {
604
+ const overlay = document.createElement("div");
605
+ overlay.className = `${InlineMessageEdit_module_css_default["overlay"] ?? ""} ${InlineMessageEdit_module_css_default["confirmationOverlay"] ?? ""}`;
606
+ const panel = document.createElement("div");
607
+ panel.className = InlineMessageEdit_module_css_default["confirmPanel"] ?? "";
608
+ panel.setAttribute("role", "alertdialog");
609
+ panel.setAttribute("aria-modal", "true");
610
+ panel.setAttribute("aria-label", "是否确认取消编辑");
611
+ const title = document.createElement("div");
612
+ title.className = InlineMessageEdit_module_css_default["title"] ?? "";
613
+ title.textContent = "是否确认取消编辑";
614
+ const footer = document.createElement("div");
615
+ footer.className = InlineMessageEdit_module_css_default["footer"] ?? "";
616
+ const yes = document.createElement("button");
617
+ yes.type = "button";
618
+ yes.textContent = "是";
619
+ const no = document.createElement("button");
620
+ no.type = "button";
621
+ no.textContent = "否";
622
+ footer.append(yes, no);
623
+ panel.append(title, footer);
624
+ overlay.appendChild(panel);
625
+ document.body.appendChild(overlay);
626
+ let mounted = true;
627
+ const dispose = () => {
628
+ if (!mounted) return;
629
+ mounted = false;
630
+ yes.removeEventListener("click", confirm);
631
+ no.removeEventListener("click", reject);
632
+ overlay.removeEventListener("keydown", onKeyDown);
633
+ overlay.remove();
634
+ };
635
+ const confirm = () => {
636
+ if (!mounted) return;
637
+ dispose();
638
+ onConfirm();
639
+ };
640
+ const reject = () => {
641
+ if (!mounted) return;
642
+ dispose();
643
+ onReject();
644
+ };
645
+ const onKeyDown = (event) => {
646
+ if (event.key === "Escape") {
647
+ event.preventDefault();
648
+ reject();
649
+ } else if (event.key === "Tab") {
650
+ event.preventDefault();
651
+ if (document.activeElement === no) yes.focus();
652
+ else no.focus();
653
+ }
654
+ };
655
+ yes.addEventListener("click", confirm);
656
+ no.addEventListener("click", reject);
657
+ overlay.addEventListener("keydown", onKeyDown);
658
+ no.focus();
659
+ return dispose;
660
+ }
661
+ //#endregion
662
+ //#region src/client/AttachmentEditor.tsx
663
+ function sizeLabel(bytes) {
664
+ if (bytes < 1024) return `${String(bytes)} B`;
665
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
666
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
667
+ }
668
+ /** 两种编辑入口共享附件草稿,关闭编辑器时仅释放新增附件。 */
669
+ function mountAttachmentEditor(container, initial, tools, onBusy, actions, options = {}) {
670
+ const items = initial.map((existing) => ({ existing }));
671
+ const list = document.createElement("div");
672
+ list.className = InlineMessageEdit_module_css_default["attachmentList"] ?? "";
673
+ list.setAttribute("aria-label", "消息附件");
674
+ const add = document.createElement("button");
675
+ add.type = "button";
676
+ add.className = `${InlineMessageEdit_module_css_default["attachmentAdd"] ?? ""} ${InlineMessageEdit_module_css_default["attachmentAddCircle"] ?? ""}`;
677
+ add.textContent = "+";
678
+ add.setAttribute("aria-label", "添加附件");
679
+ add.title = "添加图片或文件;替换附件时,先移除原附件再添加新文件。";
680
+ const picker = document.createElement("input");
681
+ picker.type = "file";
682
+ picker.multiple = true;
683
+ picker.hidden = true;
684
+ picker.setAttribute("aria-label", "选择消息附件");
685
+ const notice = document.createElement("div");
686
+ notice.className = InlineMessageEdit_module_css_default["attachmentNotice"] ?? "";
687
+ notice.setAttribute("role", "alert");
688
+ if (actions === void 0) container.append(list, ...options.hideAdd ? [] : [add], picker, notice);
689
+ else {
690
+ container.append(list, notice);
691
+ actions.prepend(...options.hideAdd ? [] : [add], picker);
692
+ }
693
+ let disposed = false;
694
+ let disabled = false;
695
+ let unsubscribe;
696
+ const imageUrls = /* @__PURE__ */ new Map();
697
+ let previewItem;
698
+ let previewRevision = 0;
699
+ let dismissPreview;
700
+ const closePreview = () => {
701
+ previewRevision += 1;
702
+ previewItem = void 0;
703
+ dismissPreview?.();
704
+ dismissPreview = void 0;
705
+ };
706
+ const imageUrl = (existing) => {
707
+ if (existing.content.type !== "image") throw new Error("此附件不是图片。");
708
+ let url = imageUrls.get(existing.blockIndex);
709
+ if (url === void 0) {
710
+ url = tools.imageUrl(existing.content.attachment);
711
+ imageUrls.set(existing.blockIndex, url);
712
+ url.catch(() => {
713
+ if (imageUrls.get(existing.blockIndex) === url) imageUrls.delete(existing.blockIndex);
714
+ });
715
+ }
716
+ return url;
717
+ };
718
+ const showPreview = (src, name, item) => {
719
+ closePreview();
720
+ previewItem = item;
721
+ const previousFocus = document.activeElement;
722
+ const overlay = document.createElement("div");
723
+ overlay.className = InlineMessageEdit_module_css_default["attachmentLightbox"] ?? "";
724
+ overlay.setAttribute("role", "dialog");
725
+ overlay.setAttribute("aria-modal", "true");
726
+ overlay.setAttribute("aria-label", "图片预览");
727
+ const image = document.createElement("img");
728
+ image.className = InlineMessageEdit_module_css_default["attachmentOriginal"] ?? "";
729
+ image.src = src;
730
+ image.alt = name;
731
+ const close = document.createElement("button");
732
+ close.type = "button";
733
+ close.className = InlineMessageEdit_module_css_default["attachmentPreviewClose"] ?? "";
734
+ close.textContent = "×";
735
+ close.setAttribute("aria-label", "关闭图片预览");
736
+ const outside = (event) => {
737
+ if (event.target === overlay) closePreview();
738
+ };
739
+ const key = (event) => {
740
+ if (event.key !== "Escape" && event.key !== "Tab") return;
741
+ event.preventDefault();
742
+ event.stopPropagation();
743
+ if (event.key === "Escape") closePreview();
744
+ else close.focus();
745
+ };
746
+ overlay.append(image, close);
747
+ overlay.addEventListener("click", outside);
748
+ close.addEventListener("click", closePreview);
749
+ document.addEventListener("keydown", key, true);
750
+ dismissPreview = () => {
751
+ document.removeEventListener("keydown", key, true);
752
+ overlay.removeEventListener("click", outside);
753
+ close.removeEventListener("click", closePreview);
754
+ overlay.remove();
755
+ if (previousFocus?.isConnected) previousFocus.focus();
756
+ };
757
+ document.body.append(overlay);
758
+ close.focus();
759
+ };
760
+ const openAttachment = async (item, name) => {
761
+ if (disabled || disposed || !items.includes(item)) return;
762
+ notice.textContent = "";
763
+ const content = "existing" in item ? item.existing.content : void 0;
764
+ const draft = "draft" in item ? item.draft : void 0;
765
+ try {
766
+ if (content?.type === "image" || draft?.kind === "image") {
767
+ closePreview();
768
+ const revision = previewRevision;
769
+ previewItem = item;
770
+ let src;
771
+ if (draft?.kind === "image") src = draft.previewUrl;
772
+ else if ("existing" in item) src = await imageUrl(item.existing);
773
+ else return;
774
+ if (!disabled && !disposed && items.includes(item) && revision === previewRevision) showPreview(src, name, item);
775
+ } else if ("existing" in item) {
776
+ if (options.openExisting === void 0) throw new Error("当前编辑入口尚不支持打开文件。");
777
+ await options.openExisting(item.existing.blockIndex);
778
+ } else {
779
+ if (options.openDraft === void 0) throw new Error("当前编辑入口尚不支持打开文件。");
780
+ await options.openDraft(item.draft);
781
+ }
782
+ } catch (error) {
783
+ if (!disposed) notice.textContent = error instanceof Error ? error.message : String(error);
784
+ }
785
+ };
786
+ const refresh = () => {
787
+ if (disposed) return;
788
+ const uploads = items.some((item) => "draft" in item && item.draft.kind === "file") ? tools.uploads() : {};
789
+ const pending = items.some((item) => "draft" in item && item.draft.kind === "file" && uploads[item.draft.id]?.status !== "ready");
790
+ const cards = items.map((item) => {
791
+ const card = document.createElement("div");
792
+ card.className = InlineMessageEdit_module_css_default["attachmentCard"] ?? "";
793
+ const content = "existing" in item ? item.existing.content : void 0;
794
+ const draft = "draft" in item ? item.draft : void 0;
795
+ const name = content?.attachment.name ?? draft?.file.name ?? "图片附件";
796
+ const bytes = content?.attachment.bytes ?? draft?.file.size ?? 0;
797
+ const isImage = content?.type === "image" || draft?.kind === "image";
798
+ const open = document.createElement("button");
799
+ open.type = "button";
800
+ open.className = InlineMessageEdit_module_css_default["attachmentOpen"] ?? "";
801
+ open.setAttribute("data-kind", isImage ? "image" : "file");
802
+ open.setAttribute("aria-label", isImage ? `放大图片:${name}` : `打开文件:${name}`);
803
+ open.title = isImage ? "点击放大图片" : "使用默认应用打开文件";
804
+ open.disabled = disabled;
805
+ open.addEventListener("click", () => {
806
+ openAttachment(item, name);
807
+ });
808
+ const preview = document.createElement(isImage ? "img" : "span");
809
+ preview.className = InlineMessageEdit_module_css_default["attachmentPreview"] ?? "";
810
+ if (isImage) {
811
+ const image = preview;
812
+ image.alt = name;
813
+ if (draft?.kind === "image") image.src = draft.previewUrl;
814
+ if (content?.type === "image" && "existing" in item) imageUrl(item.existing).then((value) => {
815
+ if (!disposed) image.src = value;
816
+ }).catch(() => {
817
+ if (!disposed) image.alt = `${name}(预览暂不可用)`;
818
+ });
819
+ } else preview.textContent = "文件";
820
+ const info = document.createElement("div");
821
+ info.className = InlineMessageEdit_module_css_default["attachmentInfo"] ?? "";
822
+ const label = document.createElement("span");
823
+ label.textContent = name;
824
+ label.title = name;
825
+ const status = document.createElement("span");
826
+ status.className = InlineMessageEdit_module_css_default["attachmentStatus"] ?? "";
827
+ const upload = draft?.kind === "file" ? uploads[draft.id] : void 0;
828
+ status.textContent = upload?.status === "error" ? `上传失败:${upload.message}` : draft?.kind === "file" && upload?.status !== "ready" ? upload?.status === "uploading" && upload.total ? `上传中 ${String(Math.round(upload.loaded / upload.total * 100))}%` : "正在上传…" : sizeLabel(bytes);
829
+ info.append(label, status);
830
+ const remove = document.createElement("button");
831
+ remove.type = "button";
832
+ remove.className = InlineMessageEdit_module_css_default["attachmentRemove"] ?? "";
833
+ remove.textContent = "×";
834
+ remove.title = `移除 ${name}`;
835
+ remove.setAttribute("aria-label", `移除附件:${name}`);
836
+ remove.disabled = disabled;
837
+ remove.addEventListener("click", () => {
838
+ if (disabled || disposed) return;
839
+ const index = items.indexOf(item);
840
+ if (index < 0) return;
841
+ items.splice(index, 1);
842
+ if (previewItem === item) closePreview();
843
+ if ("draft" in item) tools.release(item.draft.id);
844
+ refresh();
845
+ });
846
+ open.append(preview, info);
847
+ card.append(open, remove);
848
+ if (upload?.status === "error" && draft !== void 0) {
849
+ const retry = document.createElement("button");
850
+ retry.type = "button";
851
+ retry.textContent = "重试上传";
852
+ retry.disabled = disabled;
853
+ retry.addEventListener("click", () => {
854
+ if (!disabled && !disposed) tools.retry(draft.id);
855
+ });
856
+ card.append(retry);
857
+ }
858
+ return card;
859
+ });
860
+ list.replaceChildren(...cards);
861
+ add.disabled = disabled;
862
+ picker.disabled = disabled;
863
+ onBusy(pending);
864
+ };
865
+ const addFiles = (files) => {
866
+ if (disabled || disposed || files.length === 0) return;
867
+ try {
868
+ for (const draft of tools.create(files)) items.push({ draft });
869
+ unsubscribe ??= tools.subscribe(refresh);
870
+ notice.textContent = "";
871
+ refresh();
872
+ } catch (error) {
873
+ notice.textContent = error instanceof Error ? error.message : String(error);
874
+ }
875
+ };
876
+ const openPicker = () => {
877
+ if (!disabled && !disposed) picker.click();
878
+ };
879
+ const picked = () => {
880
+ try {
881
+ if (!disabled && !disposed) {
882
+ const files = Array.from(picker.files ?? []);
883
+ if (files.length > 0) (options.onFiles ?? addFiles)(files);
884
+ }
885
+ } catch (error) {
886
+ notice.textContent = error instanceof Error ? error.message : String(error);
887
+ } finally {
888
+ picker.value = "";
889
+ }
890
+ };
891
+ add.addEventListener("click", openPicker);
892
+ picker.addEventListener("change", picked);
893
+ refresh();
894
+ return {
895
+ addFiles,
896
+ pickFiles: openPicker,
897
+ serialize: async () => {
898
+ const drafts = items.flatMap((item) => "draft" in item ? [item.draft.id] : []);
899
+ const uploaded = drafts.length === 0 ? [] : await tools.serialize(drafts);
900
+ let index = 0;
901
+ return items.map((item) => {
902
+ if ("existing" in item) return {
903
+ type: "retained",
904
+ blockIndex: item.existing.blockIndex
905
+ };
906
+ const attachment = uploaded[index++];
907
+ if (attachment === void 0) throw new Error("附件尚未准备完成,请稍后重试。");
908
+ return attachment;
909
+ });
910
+ },
911
+ setDisabled: (value) => {
912
+ disabled = value;
913
+ if (value) closePreview();
914
+ refresh();
915
+ },
916
+ dispose: () => {
917
+ if (disposed) return;
918
+ disposed = true;
919
+ closePreview();
920
+ unsubscribe?.();
921
+ add.removeEventListener("click", openPicker);
922
+ picker.removeEventListener("change", picked);
923
+ for (const item of items) if ("draft" in item) tools.release(item.draft.id);
924
+ add.remove();
925
+ picker.remove();
926
+ container.replaceChildren();
927
+ }
928
+ };
929
+ }
930
+ //#endregion
931
+ //#region src/client/fileReferences.ts
932
+ const invalidPathCharacters = /[\u0000-\u001f\u007f-\u009f"]/u;
933
+ /** 使用桌面端提供的真实路径;浏览器内创建的文件没有宿主路径。 */
934
+ function filePathFor(file) {
935
+ const bridge = globalThis.__DSH_HOST_PATHS__;
936
+ try {
937
+ const path = bridge?.pathFor(file);
938
+ return path === void 0 || path === "" ? void 0 : path;
939
+ } catch {
940
+ return;
941
+ }
942
+ }
943
+ /** 对齐官方 @ 文件语法,空格使用双引号,路径分隔符统一为斜杠。 */
944
+ function formatFileReference(path) {
945
+ if (path === "" || invalidPathCharacters.test(path)) throw new Error("文件路径无法表示为 DSH 引用。");
946
+ const normalized = path.replace(/\\/gu, "/");
947
+ return /\s/u.test(normalized) ? `@"${normalized}"` : `@${normalized}`;
948
+ }
949
+ function pathFromClipboardLine(line) {
950
+ let path = line.trim();
951
+ if (path.startsWith("\"") && path.endsWith("\"")) path = path.slice(1, -1);
952
+ if (/^file:/iu.test(path)) try {
953
+ const address = new URL(path);
954
+ if (address.protocol !== "file:" || address.search !== "" || address.hash !== "") return void 0;
955
+ path = decodeURIComponent(address.pathname);
956
+ if (address.hostname !== "" && address.hostname !== "localhost") path = `//${address.hostname}${path}`;
957
+ else if (/^\/[a-z]:\//iu.test(path)) path = path.slice(1);
958
+ } catch {
959
+ return;
960
+ }
961
+ if (invalidPathCharacters.test(path)) return void 0;
962
+ if (/^\/[^/]*$/u.test(path) && !/^\/[^\s]+\.[a-z0-9]+$/iu.test(path)) return void 0;
963
+ if (!/^(?:[a-z]:[\\/]|\\\\[^\\/]+[\\/]|\/)/iu.test(path)) return void 0;
964
+ return path.replace(/\\/gu, "/");
965
+ }
966
+ /** 只读取剪贴板的路径文本;实际 File 由 filePathFor 处理,图片不会重复添加。 */
967
+ function pastedFilePaths(data) {
968
+ const result = [];
969
+ const seen = /* @__PURE__ */ new Set();
970
+ const read = (type) => {
971
+ try {
972
+ return data.getData(type);
973
+ } catch {
974
+ return "";
975
+ }
976
+ };
977
+ for (const type of ["text/uri-list", "text/plain"]) {
978
+ const paths = read(type).split(/\r\n|\n|\r/u).filter((line) => line.trim() !== "" && (type !== "text/uri-list" || !line.trimStart().startsWith("#"))).map(pathFromClipboardLine);
979
+ if (paths.some((path) => path === void 0)) continue;
980
+ for (const path of paths) {
981
+ if (path === void 0 || seen.has(path)) continue;
982
+ seen.add(path);
983
+ result.push(path);
984
+ }
985
+ }
986
+ return result;
987
+ }
988
+ /** 提取完整文件引用与文本位置;邮件地址和 @ 会话引用保留为普通文字。 */
989
+ function parseFileReferences(text) {
990
+ const result = [];
991
+ for (const match of text.matchAll(/(?:^|\s)(@(?:"([^"\r\n]+)"|([^\s"]+)))/gu)) {
992
+ const token = match[1];
993
+ const path = match[2] ?? match[3];
994
+ if (token === void 0 || path === void 0 || invalidPathCharacters.test(path)) continue;
995
+ if (path.startsWith("[") || path.startsWith("dsh-session:")) continue;
996
+ const start = match.index + match[0].length - token.length;
997
+ const normalized = path.replace(/\\/gu, "/");
998
+ const folder = normalized.endsWith("/");
999
+ const name = normalized.replace(/\/+$/u, "").split("/").at(-1) || normalized;
1000
+ result.push({
1001
+ start,
1002
+ end: start + token.length,
1003
+ path,
1004
+ token,
1005
+ label: `${name}${folder ? "/" : ""}`
1006
+ });
1007
+ }
1008
+ return result;
1009
+ }
1010
+ function sessionReferenceUri(sessionId) {
1011
+ const bytes = new TextEncoder().encode(JSON.stringify(sessionId));
1012
+ const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
1013
+ return `dsh-session:${btoa(binary).replace(/\+/gu, "-").replace(/\//gu, "_").replace(/=+$/u, "")}`;
1014
+ }
1015
+ /** 识别 canonical 会话引用;错误 URI 继续保留文字,不生成可导航 chip。 */
1016
+ function parseSessionReferences(text) {
1017
+ const result = [];
1018
+ for (const match of text.matchAll(/@\[((?:\\.|[^\\\]])*)\]\((dsh-session:[^\s)]*)\)|(dsh-session:[A-Za-z0-9_-]+)/gu)) {
1019
+ const uri = match[2] ?? match[3];
1020
+ if (uri === void 0) continue;
1021
+ const payload = uri.slice(12);
1022
+ if (!/^[A-Za-z0-9_-]+$/u.test(payload)) continue;
1023
+ try {
1024
+ const binary = atob(payload.replace(/-/gu, "+").replace(/_/gu, "/"));
1025
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
1026
+ const sessionId = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
1027
+ if (typeof sessionId !== "string" || sessionReferenceUri(sessionId) !== uri) continue;
1028
+ const token = match[0];
1029
+ const label = match[1] === void 0 ? sessionId : match[1].replace(/\\(.)/gu, "$1");
1030
+ result.push({
1031
+ start: match.index,
1032
+ end: match.index + token.length,
1033
+ sessionId,
1034
+ token,
1035
+ label
1036
+ });
1037
+ } catch {
1038
+ continue;
1039
+ }
1040
+ }
1041
+ return result;
1042
+ }
1043
+ //#endregion
1044
+ //#region src/client/RichMessageInput.ts
1045
+ function escapeHtml(text) {
1046
+ return text.replace(/[&<>"']/g, (value) => ({
1047
+ "&": "&amp;",
1048
+ "<": "&lt;",
1049
+ ">": "&gt;",
1050
+ "\"": "&quot;",
1051
+ "'": "&#39;"
1052
+ })[value]);
1053
+ }
1054
+ /** 引用以原生 @路径持久化,显示节点只负责高亮和点击。 */
1055
+ function referenceHtml(text) {
1056
+ let result = "";
1057
+ let offset = 0;
1058
+ const sessions = parseSessionReferences(text);
1059
+ const files = parseFileReferences(text).filter((file) => !sessions.some((session) => file.start >= session.start && file.start < session.end));
1060
+ for (const reference of [...files, ...sessions].sort((left, right) => left.start - right.start)) {
1061
+ result += escapeHtml(text.slice(offset, reference.start)).replace(/\n/g, "<br>");
1062
+ const location = "path" in reference ? `data-file-path="${escapeHtml(reference.path)}"` : `data-session-id="${escapeHtml(reference.sessionId)}"`;
1063
+ result += `<span class="${InlineMessageEdit_module_css_default["fileReference"] ?? ""}" contenteditable="false" role="link" tabindex="0" ${location} data-file-token="${escapeHtml(reference.token)}" title="${escapeHtml("path" in reference ? reference.path : reference.label)}">${"path" in reference ? "▤" : "@"} ${escapeHtml(reference.label)}</span>`;
1064
+ offset = reference.end;
1065
+ }
1066
+ return result + escapeHtml(text.slice(offset)).replace(/\n/g, "<br>");
1067
+ }
1068
+ /** 同时处理浏览器生成的段落和粘贴插入的换行,保留多行正文。 */
1069
+ function messageInputText(node) {
1070
+ if (node.nodeType === 3) return node.textContent ?? "";
1071
+ const element = node;
1072
+ if (element.dataset?.["fileToken"] !== void 0) return element.dataset["fileToken"];
1073
+ if (element.tagName === "BR") return "\n";
1074
+ const children = Array.from(node.childNodes ?? []);
1075
+ let result = "";
1076
+ for (const child of children) {
1077
+ const block = child.nodeType === 1 && /^(DIV|P)$/.test(child.tagName);
1078
+ if (block && result !== "" && !result.endsWith("\n")) result += "\n";
1079
+ result += messageInputText(child);
1080
+ if (block && child !== children.at(-1) && !result.endsWith("\n")) result += "\n";
1081
+ }
1082
+ return children.length === 0 ? node.textContent ?? "" : result.replace(/\u00a0/g, " ");
1083
+ }
1084
+ /** 独立内容框使用浏览器编辑历史,不绑定主会话的单例草稿。 */
1085
+ function mountRichMessageInput(container, initialText, options) {
1086
+ const input = document.createElement("div");
1087
+ input.className = `${InlineMessageEdit_module_css_default["input"] ?? ""} ${InlineMessageEdit_module_css_default["richInput"] ?? ""}`;
1088
+ input.contentEditable = "true";
1089
+ input.setAttribute("role", "textbox");
1090
+ input.setAttribute("aria-label", "消息正文");
1091
+ input.setAttribute("aria-multiline", "true");
1092
+ input.setAttribute("data-placeholder", "编辑消息,粘贴文件,输入 / 调用指令、@ 引用文件或对话");
1093
+ input.innerHTML = referenceHtml(initialText);
1094
+ container.append(input);
1095
+ let disabled = false;
1096
+ let disposed = false;
1097
+ let composing = false;
1098
+ let edited = false;
1099
+ let caret;
1100
+ const selection = () => document.getSelection();
1101
+ const remember = () => {
1102
+ const current = selection();
1103
+ if (current?.rangeCount && input.contains(current.anchorNode)) caret = current.getRangeAt(0).cloneRange();
1104
+ };
1105
+ const focus = () => {
1106
+ input.focus();
1107
+ const current = selection();
1108
+ if (current === null) return;
1109
+ const range = caret ?? document.createRange();
1110
+ if (caret === void 0) {
1111
+ range.selectNodeContents(input);
1112
+ range.collapse(false);
1113
+ }
1114
+ current.removeAllRanges();
1115
+ current.addRange(range);
1116
+ };
1117
+ const changed = () => {
1118
+ if (composing) return;
1119
+ edited = true;
1120
+ remember();
1121
+ const current = selection();
1122
+ const prefix = document.createRange();
1123
+ prefix.selectNodeContents(input);
1124
+ if (current?.rangeCount && input.contains(current.anchorNode)) {
1125
+ const range = current.getRangeAt(0);
1126
+ prefix.setEnd(range.endContainer, range.endOffset);
1127
+ }
1128
+ options.onChange?.(messageInputText(prefix.cloneContents()));
1129
+ };
1130
+ const insert = (text) => {
1131
+ if (disabled || disposed) return;
1132
+ focus();
1133
+ const current = selection();
1134
+ if (current?.rangeCount && (parseFileReferences(text)[0]?.start === 0 || parseSessionReferences(text)[0]?.start === 0)) {
1135
+ const range = current.getRangeAt(0);
1136
+ const prefix = document.createRange();
1137
+ prefix.selectNodeContents(input);
1138
+ prefix.setEnd(range.startContainer, range.startOffset);
1139
+ const before = messageInputText(prefix.cloneContents());
1140
+ if (before !== "" && !/\s$/.test(before)) text = ` ${text}`;
1141
+ }
1142
+ document.execCommand("insertHTML", false, referenceHtml(text));
1143
+ changed();
1144
+ };
1145
+ const paste = (event) => {
1146
+ if (disabled || event.clipboardData === null) return;
1147
+ const files = Array.from(event.clipboardData.files);
1148
+ if (!files.length) for (const item of Array.from(event.clipboardData.items)) {
1149
+ const file = item.kind === "file" ? item.getAsFile() : null;
1150
+ if (file !== null) files.push(file);
1151
+ }
1152
+ const paths = files.length === 0 ? pastedFilePaths(event.clipboardData) : [];
1153
+ event.preventDefault();
1154
+ if (files.length) options.addFiles(files);
1155
+ else if (paths.length) options.addPaths(paths);
1156
+ else insert(event.clipboardData.getData("text/plain"));
1157
+ };
1158
+ const drop = (event) => {
1159
+ if (disabled || event.dataTransfer === null) return;
1160
+ event.preventDefault();
1161
+ const files = Array.from(event.dataTransfer.files);
1162
+ if (files.length) options.addFiles(files);
1163
+ else {
1164
+ const paths = pastedFilePaths(event.dataTransfer);
1165
+ if (paths.length) options.addPaths(paths);
1166
+ else insert(event.dataTransfer.getData("text/plain"));
1167
+ }
1168
+ };
1169
+ const dragOver = (event) => {
1170
+ if (!disabled) event.preventDefault();
1171
+ };
1172
+ const openReference = (event) => {
1173
+ const chip = event.target.closest("[data-file-path], [data-session-id]");
1174
+ if (chip === null || !input.contains(chip)) return;
1175
+ if (event instanceof KeyboardEvent && event.key !== "Enter" && event.key !== " ") return;
1176
+ event.preventDefault();
1177
+ const path = chip.dataset["filePath"];
1178
+ if (path !== void 0) options.openPath(path).catch(options.onError);
1179
+ const sessionId = chip.dataset["sessionId"];
1180
+ if (sessionId !== void 0) options.openSession(sessionId);
1181
+ };
1182
+ const keydown = (event) => {
1183
+ if (composing || event.isComposing || event.keyCode === 229 || disabled) return;
1184
+ if (event.target.dataset["fileToken"] !== void 0) {
1185
+ openReference(event);
1186
+ return;
1187
+ }
1188
+ if (options.onKeydown?.(event)) return;
1189
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
1190
+ event.preventDefault();
1191
+ options.onSave();
1192
+ } else if (event.key === "Enter" && !event.shiftKey && !event.altKey) {
1193
+ event.preventDefault();
1194
+ options.onSend();
1195
+ }
1196
+ };
1197
+ const copy = (event) => {
1198
+ const current = selection();
1199
+ if (current?.rangeCount && input.contains(current.anchorNode) && event.clipboardData !== null) {
1200
+ event.clipboardData.setData("text/plain", messageInputText(current.getRangeAt(0).cloneContents()));
1201
+ event.preventDefault();
1202
+ if (event.type === "cut" && !disabled) document.execCommand("delete");
1203
+ }
1204
+ };
1205
+ const compositionStart = () => {
1206
+ composing = true;
1207
+ };
1208
+ const compositionEnd = () => {
1209
+ composing = false;
1210
+ changed();
1211
+ };
1212
+ input.addEventListener("compositionstart", compositionStart);
1213
+ input.addEventListener("compositionend", compositionEnd);
1214
+ input.addEventListener("input", changed);
1215
+ input.addEventListener("keyup", remember);
1216
+ input.addEventListener("mouseup", remember);
1217
+ input.addEventListener("blur", remember);
1218
+ input.addEventListener("paste", paste);
1219
+ input.addEventListener("drop", drop);
1220
+ input.addEventListener("dragover", dragOver);
1221
+ input.addEventListener("click", openReference);
1222
+ input.addEventListener("keydown", keydown);
1223
+ input.addEventListener("copy", copy);
1224
+ input.addEventListener("cut", copy);
1225
+ return {
1226
+ text: () => edited ? messageInputText(input) : initialText,
1227
+ insert,
1228
+ focus,
1229
+ replaceTrailing: (token, text) => {
1230
+ if (disabled || disposed) return;
1231
+ focus();
1232
+ const current = selection();
1233
+ if (current === null) return;
1234
+ for (let index = 0; index < token.length; index++) current.modify("extend", "backward", "character");
1235
+ remember();
1236
+ insert(text);
1237
+ },
1238
+ setDisabled: (value) => {
1239
+ disabled = value;
1240
+ input.contentEditable = String(!value);
1241
+ input.setAttribute("aria-disabled", String(value));
1242
+ },
1243
+ dispose: () => {
1244
+ disposed = true;
1245
+ input.removeEventListener("compositionstart", compositionStart);
1246
+ input.removeEventListener("compositionend", compositionEnd);
1247
+ input.removeEventListener("input", changed);
1248
+ input.removeEventListener("keyup", remember);
1249
+ input.removeEventListener("mouseup", remember);
1250
+ input.removeEventListener("blur", remember);
1251
+ input.removeEventListener("paste", paste);
1252
+ input.removeEventListener("drop", drop);
1253
+ input.removeEventListener("dragover", dragOver);
1254
+ input.removeEventListener("click", openReference);
1255
+ input.removeEventListener("keydown", keydown);
1256
+ input.removeEventListener("copy", copy);
1257
+ input.removeEventListener("cut", copy);
1258
+ input.remove();
1259
+ }
1260
+ };
1261
+ }
1262
+ //#endregion
1263
+ //#region src/client/MessageComposer.ts
1264
+ function button(label, className) {
1265
+ const element = document.createElement("button");
1266
+ element.type = "button";
1267
+ element.textContent = label;
1268
+ if (className !== void 0) element.className = className;
1269
+ return element;
1270
+ }
1271
+ /** 弹窗与 Timeline 共用独立的正文、附件及提交选项。 */
1272
+ function mountMessageComposer(container, block, attachmentTools, tools, events) {
1273
+ const frame = document.createElement("div");
1274
+ frame.className = InlineMessageEdit_module_css_default["composer"] ?? "";
1275
+ const attachments = document.createElement("div");
1276
+ const inputContainer = document.createElement("div");
1277
+ const toolbar = document.createElement("div");
1278
+ toolbar.className = InlineMessageEdit_module_css_default["composerToolbar"] ?? "";
1279
+ const plus = button("+", InlineMessageEdit_module_css_default["composerPlus"]);
1280
+ plus.setAttribute("aria-label", "添加文件");
1281
+ plus.title = "添加文件";
1282
+ const permission = button("权限 ▾", InlineMessageEdit_module_css_default["composerControl"]);
1283
+ permission.setAttribute("aria-label", "修改权限");
1284
+ const model = button("模型 ▾", InlineMessageEdit_module_css_default["composerModel"]);
1285
+ model.setAttribute("aria-label", "修改模型和推理等级");
1286
+ const status = document.createElement("div");
1287
+ status.className = InlineMessageEdit_module_css_default["attachmentNotice"] ?? "";
1288
+ status.setAttribute("role", "status");
1289
+ const popup = document.createElement("div");
1290
+ popup.className = InlineMessageEdit_module_css_default["composerMenu"] ?? "";
1291
+ popup.hidden = true;
1292
+ const suggestions = document.createElement("div");
1293
+ suggestions.className = InlineMessageEdit_module_css_default["referenceSuggestions"] ?? "";
1294
+ suggestions.hidden = true;
1295
+ toolbar.append(plus, permission, model);
1296
+ frame.append(attachments, inputContainer, suggestions, popup, toolbar);
1297
+ container.append(frame, status);
1298
+ let disabled = false;
1299
+ let disposed = false;
1300
+ let options;
1301
+ let draftSettings = {};
1302
+ let search;
1303
+ let searchTimer;
1304
+ let highlighted = 0;
1305
+ let richInput;
1306
+ let attachmentEditor;
1307
+ const showError = (error) => {
1308
+ if (!disposed) status.textContent = error instanceof Error ? error.message : String(error);
1309
+ };
1310
+ const addPaths = (paths) => {
1311
+ richInput.insert(paths.map((path) => formatFileReference(path)).join(" ") + " ");
1312
+ };
1313
+ const addFiles = (files) => {
1314
+ if (disabled || disposed) return;
1315
+ const uploads = [];
1316
+ const paths = [];
1317
+ for (const file of files) {
1318
+ const path = filePathFor(file);
1319
+ if (path !== void 0 && !/^image\/(png|jpeg|webp|gif)$/.test(file.type)) paths.push(path);
1320
+ else uploads.push(file);
1321
+ }
1322
+ if (paths.length) addPaths(paths);
1323
+ if (uploads.length) attachmentEditor.addFiles(uploads);
1324
+ };
1325
+ const closeMenu = () => {
1326
+ popup.hidden = true;
1327
+ permission.setAttribute("aria-expanded", "false");
1328
+ model.setAttribute("aria-expanded", "false");
1329
+ };
1330
+ const beginMenu = (label, kind = "command") => {
1331
+ popup.replaceChildren();
1332
+ popup.setAttribute("role", "menu");
1333
+ popup.setAttribute("aria-label", label);
1334
+ popup.dataset["kind"] = kind;
1335
+ popup.hidden = false;
1336
+ permission.setAttribute("aria-expanded", String(kind === "permission"));
1337
+ model.setAttribute("aria-expanded", String(kind === "model" || kind === "modelList"));
1338
+ };
1339
+ const menuItem = (label, description, act, selected, target = popup) => {
1340
+ const item = button("");
1341
+ const text = document.createElement("span");
1342
+ text.className = InlineMessageEdit_module_css_default["menuItemLabel"] ?? "";
1343
+ text.textContent = label;
1344
+ item.setAttribute("role", selected === void 0 ? "menuitem" : "menuitemradio");
1345
+ if (selected !== void 0) item.setAttribute("aria-checked", String(selected));
1346
+ item.title = label;
1347
+ const hint = document.createElement("span");
1348
+ hint.className = InlineMessageEdit_module_css_default["menuValue"] ?? "";
1349
+ hint.textContent = selected === void 0 ? description : selected ? "✓" : "";
1350
+ if (selected !== void 0) hint.setAttribute("aria-hidden", "true");
1351
+ item.append(text, hint);
1352
+ item.disabled = disabled;
1353
+ item.addEventListener("click", () => {
1354
+ closeMenu();
1355
+ act();
1356
+ });
1357
+ target.append(item);
1358
+ return item;
1359
+ };
1360
+ const permissionLabel = (id) => {
1361
+ return id === void 0 ? "权限" : {
1362
+ "read-only": "仅可查看",
1363
+ "workspace-write": "工作区内修改",
1364
+ "danger-full-access": "完全权限",
1365
+ "auto-review": "Auto review"
1366
+ }[id] ?? options?.permissions.find((preset) => preset.id === id)?.label ?? (id === "custom" ? "自定义权限" : id);
1367
+ };
1368
+ const effortLabel = (effort) => {
1369
+ if (!effort) return "默认";
1370
+ const values = {
1371
+ ...options?.current,
1372
+ ...draftSettings
1373
+ };
1374
+ return (options?.models.find((entry) => entry.provider === values.provider && entry.model === values.model))?.reasoningEffortLabels?.[effort] ?? {
1375
+ off: "Off",
1376
+ low: "Low",
1377
+ medium: "Medium",
1378
+ high: "High",
1379
+ max: "Max"
1380
+ }[effort] ?? effort;
1381
+ };
1382
+ const updateControls = () => {
1383
+ const values = {
1384
+ ...options?.current,
1385
+ ...draftSettings
1386
+ };
1387
+ permission.textContent = `${permissionLabel(values.permissionPreset)} ▾`;
1388
+ const selected = options?.models.find((entry) => entry.provider === values.provider && entry.model === values.model);
1389
+ model.textContent = `${selected?.label ?? values.model ?? "模型"}${values.reasoningEffort ? ` ${effortLabel(values.reasoningEffort)}` : ""} ▾`;
1390
+ model.title = model.textContent;
1391
+ };
1392
+ const permissionMenu = () => {
1393
+ beginMenu("选择权限", "permission");
1394
+ const values = {
1395
+ ...options?.current,
1396
+ ...draftSettings
1397
+ };
1398
+ for (const preset of options?.permissions ?? []) {
1399
+ const item = menuItem(permissionLabel(preset.id), "", () => {
1400
+ draftSettings.permissionPreset = preset.id;
1401
+ updateControls();
1402
+ richInput.focus();
1403
+ }, preset.id === values.permissionPreset);
1404
+ item.title = preset.description ?? permissionLabel(preset.id);
1405
+ }
1406
+ if (options === void 0) menuItem("重新加载选项", "", () => {
1407
+ loadOptions();
1408
+ });
1409
+ };
1410
+ const modelMenu = () => {
1411
+ beginMenu("选择模型和推理等级", "model");
1412
+ const values = {
1413
+ ...options?.current,
1414
+ ...draftSettings
1415
+ };
1416
+ const selected = options?.models.find((entry) => entry.provider === values.provider && entry.model === values.model);
1417
+ menuItem("模型", `${selected?.label ?? values.model ?? "选择模型"} ›`, modelList);
1418
+ if (selected?.reasoningEfforts.length) menuItem("推理等级", `${effortLabel(values.reasoningEffort)} ›`, () => {
1419
+ beginMenu("选择推理等级", "model");
1420
+ menuItem("‹ 推理等级", "", modelMenu);
1421
+ for (const effort of ["", ...selected.reasoningEfforts]) menuItem(effortLabel(effort), "", () => {
1422
+ draftSettings = {
1423
+ ...draftSettings,
1424
+ provider: selected.provider,
1425
+ model: selected.model,
1426
+ reasoningEffort: effort
1427
+ };
1428
+ updateControls();
1429
+ richInput.focus();
1430
+ }, effort === (values.reasoningEffort ?? ""));
1431
+ });
1432
+ if (options === void 0) menuItem("重新加载选项", "", () => {
1433
+ loadOptions();
1434
+ });
1435
+ };
1436
+ const modelList = () => {
1437
+ beginMenu("选择模型", "modelList");
1438
+ const back = menuItem("‹ 模型", "", modelMenu);
1439
+ back.className = InlineMessageEdit_module_css_default["menuBack"] ?? "";
1440
+ const field = document.createElement("input");
1441
+ field.placeholder = "搜索模型…";
1442
+ field.setAttribute("aria-label", "搜索模型");
1443
+ field.className = InlineMessageEdit_module_css_default["modelSearch"] ?? "";
1444
+ const list = document.createElement("div");
1445
+ list.className = InlineMessageEdit_module_css_default["modelList"] ?? "";
1446
+ popup.append(field, list);
1447
+ const render = () => {
1448
+ list.replaceChildren();
1449
+ const values = {
1450
+ ...options?.current,
1451
+ ...draftSettings
1452
+ };
1453
+ const query = field.value.trim().toLocaleLowerCase();
1454
+ const groups = /* @__PURE__ */ new Map();
1455
+ for (const entry of options?.models ?? []) {
1456
+ if (query && !`${entry.label} ${entry.model} ${entry.providerLabel ?? entry.provider}`.toLocaleLowerCase().includes(query)) continue;
1457
+ const group = groups.get(entry.provider) ?? [];
1458
+ group.push(entry);
1459
+ groups.set(entry.provider, group);
1460
+ }
1461
+ for (const entries of groups.values()) {
1462
+ const heading = document.createElement("div");
1463
+ heading.className = InlineMessageEdit_module_css_default["menuHeading"] ?? "";
1464
+ heading.textContent = entries[0]?.providerLabel ?? entries[0]?.provider ?? "";
1465
+ list.append(heading);
1466
+ for (const entry of entries) menuItem(entry.label, "", () => {
1467
+ draftSettings = {
1468
+ ...draftSettings,
1469
+ provider: entry.provider,
1470
+ model: entry.model
1471
+ };
1472
+ if (!entry.reasoningEfforts.includes(values.reasoningEffort ?? "")) draftSettings.reasoningEffort = "";
1473
+ updateControls();
1474
+ richInput.focus();
1475
+ }, entry.provider === values.provider && entry.model === values.model, list);
1476
+ }
1477
+ if (!groups.size) {
1478
+ const empty = document.createElement("div");
1479
+ empty.className = InlineMessageEdit_module_css_default["menuHeading"] ?? "";
1480
+ empty.textContent = "没有找到匹配的模型";
1481
+ list.append(empty);
1482
+ }
1483
+ };
1484
+ field.addEventListener("input", render);
1485
+ render();
1486
+ field.focus();
1487
+ };
1488
+ const executeCommand = (line) => {
1489
+ if (disabled) return;
1490
+ status.textContent = "正在执行指令…";
1491
+ tools.command(line).then(() => {
1492
+ if (!disposed) status.textContent = "指令已执行。";
1493
+ }).catch(showError);
1494
+ };
1495
+ const commandForm = (name, label, placeholder) => {
1496
+ beginMenu(label);
1497
+ const field = document.createElement("input");
1498
+ field.placeholder = placeholder;
1499
+ field.setAttribute("aria-label", label);
1500
+ const run = button(label);
1501
+ run.addEventListener("click", () => {
1502
+ closeMenu();
1503
+ executeCommand(`/${name}${field.value.trim() ? ` ${field.value.trim()}` : ""}`);
1504
+ });
1505
+ popup.append(field, run);
1506
+ field.focus();
1507
+ };
1508
+ const planMenu = () => {
1509
+ beginMenu("计划模式");
1510
+ menuItem("进入计划模式", "先规划,再执行", () => executeCommand("/plan"));
1511
+ menuItem("退出计划模式", "恢复正常对话", () => executeCommand("/plan off"));
1512
+ };
1513
+ const chooseCommand = (name) => {
1514
+ switch (name) {
1515
+ case "file":
1516
+ attachmentEditor.pickFiles();
1517
+ break;
1518
+ case "goal":
1519
+ commandForm("goal", "设置或查看目标", "输入目标;留空查看当前目标");
1520
+ break;
1521
+ case "plan":
1522
+ planMenu();
1523
+ break;
1524
+ case "feedback":
1525
+ commandForm("feedback", "发送反馈", "输入反馈内容");
1526
+ break;
1527
+ case "permission":
1528
+ permissionMenu();
1529
+ break;
1530
+ case "model":
1531
+ modelMenu();
1532
+ break;
1533
+ default: executeCommand(`/${name}`);
1534
+ }
1535
+ };
1536
+ const suggestReferences = (text) => {
1537
+ search?.abort();
1538
+ if (searchTimer !== void 0) clearTimeout(searchTimer);
1539
+ suggestions.hidden = true;
1540
+ const slash = /^\/([^\s]*)$/.exec(text);
1541
+ const match = /(?:^|\s)@([^\s@]*)$/.exec(text);
1542
+ if (slash !== null) {
1543
+ const controller = new AbortController();
1544
+ search = controller;
1545
+ tools.commands(slash[1] ?? "", controller.signal).then((commands) => {
1546
+ if (disposed || controller.signal.aborted || !commands.length) return;
1547
+ suggestions.replaceChildren();
1548
+ suggestions.setAttribute("role", "listbox");
1549
+ suggestions.setAttribute("aria-label", "指令候选");
1550
+ for (const command of commands.slice(0, 12)) {
1551
+ const item = button(`${command.label ?? command.name} /${command.name}`);
1552
+ item.setAttribute("role", "option");
1553
+ item.addEventListener("mousedown", (event) => event.preventDefault());
1554
+ item.addEventListener("click", () => {
1555
+ suggestions.hidden = true;
1556
+ richInput.replaceTrailing(`/${slash[1] ?? ""}`, "");
1557
+ chooseCommand(command.name);
1558
+ });
1559
+ suggestions.append(item);
1560
+ }
1561
+ highlighted = 0;
1562
+ suggestions.hidden = false;
1563
+ suggestions.children[0]?.setAttribute("aria-selected", "true");
1564
+ }).catch((error) => {
1565
+ if (!controller.signal.aborted) showError(error);
1566
+ });
1567
+ return;
1568
+ }
1569
+ if (match === null) return;
1570
+ const query = match[1] ?? "";
1571
+ searchTimer = setTimeout(() => {
1572
+ const controller = new AbortController();
1573
+ search = controller;
1574
+ tools.references(query, controller.signal).then((results) => {
1575
+ if (disposed || controller.signal.aborted || !results.length) return;
1576
+ suggestions.replaceChildren();
1577
+ suggestions.setAttribute("role", "listbox");
1578
+ suggestions.setAttribute("aria-label", "文件引用候选");
1579
+ for (const reference of results.slice(0, 12)) {
1580
+ const item = button(`${reference.kind === "folder" ? "▱" : reference.kind === "session" ? "@" : "▤"} ${reference.label}`);
1581
+ item.title = reference.kind === "session" ? reference.label : reference.path;
1582
+ item.setAttribute("role", "option");
1583
+ item.addEventListener("mousedown", (event) => event.preventDefault());
1584
+ item.addEventListener("click", () => {
1585
+ richInput.replaceTrailing(`@${query}`, `${reference.kind === "session" ? reference.mention : formatFileReference(reference.path)} `);
1586
+ suggestions.hidden = true;
1587
+ });
1588
+ suggestions.append(item);
1589
+ }
1590
+ suggestions.hidden = false;
1591
+ highlighted = 0;
1592
+ suggestions.children[0]?.setAttribute("aria-selected", "true");
1593
+ }).catch((error) => {
1594
+ if (!controller.signal.aborted) showError(error);
1595
+ });
1596
+ }, 150);
1597
+ };
1598
+ richInput = mountRichMessageInput(inputContainer, block.text, {
1599
+ addFiles,
1600
+ addPaths,
1601
+ openPath: tools.openPath,
1602
+ openSession: tools.openSession,
1603
+ onError: showError,
1604
+ onSend: events.onSend,
1605
+ onSave: events.onSave,
1606
+ onChange: suggestReferences,
1607
+ onKeydown: (event) => {
1608
+ if (suggestions.hidden) return false;
1609
+ if (event.key === "Escape") {
1610
+ suggestions.hidden = true;
1611
+ event.preventDefault();
1612
+ return true;
1613
+ }
1614
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
1615
+ const direction = event.key === "ArrowUp" ? -1 : 1;
1616
+ highlighted = (highlighted + direction + suggestions.children.length) % suggestions.children.length;
1617
+ for (const [index, item] of Array.from(suggestions.children).entries()) item.setAttribute("aria-selected", String(index === highlighted));
1618
+ event.preventDefault();
1619
+ return true;
1620
+ }
1621
+ if (event.key === "Enter" || event.key === "Tab") {
1622
+ suggestions.children[highlighted]?.click();
1623
+ event.preventDefault();
1624
+ return true;
1625
+ }
1626
+ return false;
1627
+ }
1628
+ });
1629
+ attachmentEditor = mountAttachmentEditor(attachments, block.attachments ?? [], attachmentTools, events.onBusy, void 0, {
1630
+ hideAdd: true,
1631
+ onFiles: addFiles,
1632
+ openExisting: (blockIndex) => tools.openAttachment(block.eventSeq, blockIndex),
1633
+ openDraft: async (draft) => {
1634
+ const path = filePathFor(draft.file);
1635
+ if (path !== void 0) return tools.openPath(path);
1636
+ const [payload] = await attachmentTools.serialize([draft.id]);
1637
+ if (payload?.type !== "file") throw new Error("附件尚未准备完成,请稍后重试。");
1638
+ await tools.openUpload(payload.receiptId);
1639
+ }
1640
+ });
1641
+ const loadOptions = async () => {
1642
+ try {
1643
+ const result = await tools.options();
1644
+ if (!Array.isArray(result.models) || !Array.isArray(result.permissions)) throw new Error("宿主尚未提供编辑选项,请安装新版插件并重启 DSH。");
1645
+ if (disposed) return;
1646
+ options = result;
1647
+ updateControls();
1648
+ } catch (error) {
1649
+ showError(error);
1650
+ }
1651
+ };
1652
+ const outside = (event) => {
1653
+ const path = event.composedPath();
1654
+ if (!path.includes(popup) && !path.includes(plus) && !path.includes(permission) && !path.includes(model)) closeMenu();
1655
+ };
1656
+ const togglePermission = () => {
1657
+ if (!popup.hidden && popup.getAttribute("aria-label") === "选择权限") closeMenu();
1658
+ else permissionMenu();
1659
+ };
1660
+ const toggleModel = () => {
1661
+ if (!popup.hidden && (popup.dataset["kind"] === "model" || popup.dataset["kind"] === "modelList")) closeMenu();
1662
+ else modelMenu();
1663
+ };
1664
+ frame.addEventListener("keydown", (event) => {
1665
+ if (event.isComposing || event.keyCode === 229) return;
1666
+ if (event.key === "Escape" && !popup.hidden) {
1667
+ closeMenu();
1668
+ richInput.focus();
1669
+ event.stopPropagation();
1670
+ }
1671
+ });
1672
+ plus.addEventListener("click", () => {
1673
+ closeMenu();
1674
+ attachmentEditor.pickFiles();
1675
+ });
1676
+ permission.addEventListener("click", togglePermission);
1677
+ model.addEventListener("click", toggleModel);
1678
+ document.addEventListener("click", outside);
1679
+ loadOptions();
1680
+ return {
1681
+ text: richInput.text,
1682
+ settings: (regenerate) => {
1683
+ const settings = {
1684
+ ...regenerate ? options?.current : {},
1685
+ ...draftSettings
1686
+ };
1687
+ if (settings.permissionPreset !== void 0 && !options?.permissions.some((item) => item.id === settings.permissionPreset)) delete settings.permissionPreset;
1688
+ if (settings.reasoningEffort === "") delete settings.reasoningEffort;
1689
+ return Object.keys(settings).length ? settings : void 0;
1690
+ },
1691
+ serialize: () => attachmentEditor.serialize(),
1692
+ focus: richInput.focus,
1693
+ setDisabled: (value) => {
1694
+ disabled = value;
1695
+ richInput.setDisabled(value);
1696
+ attachmentEditor.setDisabled(value);
1697
+ plus.disabled = value;
1698
+ permission.disabled = value;
1699
+ model.disabled = value;
1700
+ if (value) {
1701
+ closeMenu();
1702
+ suggestions.hidden = true;
1703
+ }
1704
+ },
1705
+ dispose: () => {
1706
+ disposed = true;
1707
+ search?.abort();
1708
+ if (searchTimer !== void 0) clearTimeout(searchTimer);
1709
+ document.removeEventListener("click", outside);
1710
+ attachmentEditor.dispose();
1711
+ richInput.dispose();
1712
+ frame.remove();
1713
+ status.remove();
1714
+ }
1715
+ };
1716
+ }
1717
+ //#endregion
1718
+ //#region src/client/InlineMessageEdit.tsx
1719
+ /**
1720
+ * Message-row edit affordance: injects retry + edit icon buttons into each
1721
+ * settled message's icon-actions row (the official MessageIconActions has no
1722
+ * plugin slot, so injection rides a MutationObserver over action rows).
1723
+ * Icons are the official outline-16 SVGs inlined to avoid bundling the
1724
+ * primitives package.
1725
+ */
1726
+ const BLOCK_TITLE = {
1727
+ user: "编辑用户消息",
1728
+ "assistant.reasoning": "编辑助手思考",
1729
+ "assistant.response": "编辑助手回复"
1730
+ };
1731
+ const STYLE = {
1732
+ overlay: InlineMessageEdit_module_css_default["overlay"] ?? "",
1733
+ panel: InlineMessageEdit_module_css_default["panel"] ?? "",
1734
+ title: InlineMessageEdit_module_css_default["title"] ?? "",
1735
+ input: InlineMessageEdit_module_css_default["input"] ?? "",
1736
+ footer: InlineMessageEdit_module_css_default["footer"] ?? "",
1737
+ iconButton: InlineMessageEdit_module_css_default["iconButton"] ?? "",
1738
+ picker: InlineMessageEdit_module_css_default["picker"] ?? "",
1739
+ pickerItem: InlineMessageEdit_module_css_default["pickerItem"] ?? "",
1740
+ pickerItemActive: InlineMessageEdit_module_css_default["pickerItemActive"] ?? ""
1741
+ };
1742
+ /** Official ic_ds_refresh_outline_16 path (dsh-client-ui-primitives). */
1743
+ const REFRESH_PATH = "M7.92136 0.349152C10.3744 0.349234 12.5564 1.5052 13.9557 3.29894L15.1281 2.12759C15.3303 1.92546 15.6767 2.06943 15.6767 2.35538V5.53923C15.6766 5.71626 15.5329 5.85976 15.3559 5.86002H12.171C11.8854 5.8597 11.7426 5.51465 11.9443 5.31249L12.9641 4.29056C11.8237 2.74305 9.98908 1.74106 7.92136 1.74097C4.46436 1.74097 1.66233 4.543 1.66233 8C1.66233 11.457 4.46436 14.259 7.92136 14.259C11.3782 14.2589 14.1804 11.4569 14.1804 8H15.5722C15.5722 12.2251 12.1465 15.6507 7.92136 15.6508C3.69614 15.6508 0.270508 12.2252 0.270508 8C0.270508 3.77478 3.69614 0.349152 7.92136 0.349152Z";
1744
+ /** Official ic_ds_edit_outline_16 path (dsh-client-ui-primitives). */
1745
+ const EDIT_PATH = "M9.94076 1.34942C10.7047 0.90231 11.6503 0.902415 12.4143 1.34942C12.7061 1.52015 12.9688 1.79118 13.3104 2.13284C13.6521 2.47448 13.9231 2.73721 14.0939 3.02894C14.5408 3.79294 14.5409 4.73856 14.0939 5.50251C13.9231 5.79415 13.652 6.05704 13.3104 6.39861L6.65932 13.0497C6.28068 13.4284 6.00695 13.7108 5.66543 13.9097C5.32391 14.1085 4.94315 14.2074 4.42705 14.3498L3.24394 14.6761C2.77527 14.8054 2.34538 14.9262 2.00131 14.9684C1.65196 15.0112 1.17964 15.0013 0.810764 14.6325C0.441921 14.2637 0.432107 13.7913 0.47486 13.442C0.517035 13.0979 0.6379 12.668 0.767181 12.1993L1.09352 11.0162C1.23588 10.5001 1.33481 10.1193 1.5336 9.77784C1.7325 9.43632 2.0149 9.1626 2.39355 8.78395L9.04466 2.13284C9.38625 1.79126 9.64911 1.52016 9.94076 1.34942ZM15.5427 14.8398H7.55223L8.96707 13.425H15.5427V14.8398ZM3.39382 9.78422C2.965 10.213 2.84244 10.3436 2.75709 10.49C2.67183 10.6366 2.61862 10.8079 2.45733 11.3925L2.13099 12.5756C2.00183 13.0439 1.92194 13.3419 1.88863 13.5536C2.10041 13.5204 2.39872 13.4416 2.86764 13.3123L4.05075 12.9859C4.63544 12.8246 4.80669 12.7715 4.95323 12.6862C5.09968 12.6008 5.23022 12.4783 5.65905 12.0494L10.721 6.98644L8.45577 4.72121L3.39382 9.78422ZM11.7 2.57079C11.3774 2.38198 10.9777 2.38198 10.6551 2.57079C10.5602 2.62647 10.4487 2.72931 10.0449 3.13311L9.45604 3.72094L11.7213 5.98617L12.3102 5.39833C12.7139 4.99457 12.8168 4.88307 12.8725 4.78818C13.0613 4.46561 13.0612 4.06585 12.8725 3.74326C12.8169 3.64827 12.7146 3.53752 12.3102 3.13311C11.9057 2.72863 11.795 2.6264 11.7 2.57079Z";
1746
+ function svgIcon(path) {
1747
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1748
+ svg.setAttribute("width", "16");
1749
+ svg.setAttribute("height", "16");
1750
+ svg.setAttribute("viewBox", "0 0 16 16");
1751
+ svg.setAttribute("fill", "none");
1752
+ const p = document.createElementNS("http://www.w3.org/2000/svg", "path");
1753
+ p.setAttribute("d", path);
1754
+ p.setAttribute("fill", "currentColor");
1755
+ svg.appendChild(p);
1756
+ return svg;
1757
+ }
1758
+ function blockTitle(kind) {
1759
+ return BLOCK_TITLE[kind] ?? "编辑消息";
1760
+ }
1761
+ /** 编辑器支持用户消息只保存或保存并发送,关闭前确认取消。 */
1762
+ function mountEditor(block, edit, close, attachmentTools, composerTools) {
1763
+ const overlay = document.createElement("div");
1764
+ overlay.className = STYLE.overlay;
1765
+ const panel = document.createElement("div");
1766
+ panel.className = STYLE.panel;
1767
+ panel.setAttribute("role", "dialog");
1768
+ panel.setAttribute("aria-modal", "true");
1769
+ panel.setAttribute("aria-label", blockTitle(block.kind));
1770
+ const title = document.createElement("div");
1771
+ title.className = STYLE.title;
1772
+ title.textContent = blockTitle(block.kind);
1773
+ const input = document.createElement("textarea");
1774
+ input.className = STYLE.input;
1775
+ input.value = block.text;
1776
+ input.setAttribute("aria-label", "消息正文");
1777
+ const attachments = document.createElement("div");
1778
+ attachments.className = InlineMessageEdit_module_css_default["attachmentEditor"] ?? "";
1779
+ const error = document.createElement("div");
1780
+ error.className = InlineMessageEdit_module_css_default["attachmentNotice"] ?? "";
1781
+ error.setAttribute("role", "alert");
1782
+ const footer = document.createElement("div");
1783
+ footer.className = STYLE.footer;
1784
+ const save = document.createElement("button");
1785
+ save.type = "button";
1786
+ save.textContent = "保存";
1787
+ const send = block.kind === "user" ? document.createElement("button") : void 0;
1788
+ if (send !== void 0) {
1789
+ send.type = "button";
1790
+ send.textContent = "保存并发送";
1791
+ }
1792
+ const cancel = document.createElement("button");
1793
+ cancel.type = "button";
1794
+ cancel.textContent = "取消";
1795
+ footer.append(save, ...send === void 0 ? [] : [send], cancel);
1796
+ panel.append(title, ...block.kind === "user" ? [attachments] : [input], error, footer);
1797
+ overlay.appendChild(panel);
1798
+ document.body.appendChild(overlay);
1799
+ let mounted = true;
1800
+ let saving = false;
1801
+ let uploading = false;
1802
+ let composer;
1803
+ let closeConfirmation;
1804
+ const updateButtons = () => {
1805
+ save.disabled = saving || uploading;
1806
+ if (send !== void 0) send.disabled = saving || uploading;
1807
+ cancel.disabled = saving;
1808
+ input.readOnly = saving;
1809
+ };
1810
+ const applyEdit = (regenerate) => {
1811
+ if (saving || uploading) return;
1812
+ saving = true;
1813
+ updateButtons();
1814
+ composer?.setDisabled(true);
1815
+ error.textContent = "";
1816
+ (async () => {
1817
+ try {
1818
+ const payload = await composer?.serialize();
1819
+ if (!mounted) return;
1820
+ const applied = await edit(block, composer?.text() ?? input.value, "truncate", regenerate, payload, composer?.settings(regenerate));
1821
+ if (!mounted) return;
1822
+ if (applied) close();
1823
+ else error.textContent = "保存失败,请检查消息状态后重试。";
1824
+ } catch (cause) {
1825
+ if (mounted) error.textContent = cause instanceof Error ? cause.message : String(cause);
1826
+ } finally {
1827
+ if (mounted) {
1828
+ saving = false;
1829
+ composer?.setDisabled(false);
1830
+ updateButtons();
1831
+ }
1832
+ }
1833
+ })();
1834
+ };
1835
+ const saveEdit = () => {
1836
+ applyEdit(block.kind !== "user");
1837
+ };
1838
+ const saveAndSend = () => {
1839
+ applyEdit(true);
1840
+ };
1841
+ const cancelEdit = () => {
1842
+ if (saving || closeConfirmation !== void 0) return;
1843
+ closeConfirmation = confirmCancelEdit(() => {
1844
+ closeConfirmation = void 0;
1845
+ close();
1846
+ }, () => {
1847
+ closeConfirmation = void 0;
1848
+ if (composer === void 0) input.focus();
1849
+ else composer.focus();
1850
+ });
1851
+ };
1852
+ const dismiss = (event) => {
1853
+ if (event.target === overlay) cancelEdit();
1854
+ };
1855
+ if (block.kind === "user") {
1856
+ composer = mountMessageComposer(attachments, block, attachmentTools, composerTools, {
1857
+ onBusy: (busy) => {
1858
+ uploading = busy;
1859
+ updateButtons();
1860
+ },
1861
+ onSave: saveEdit,
1862
+ onSend: saveAndSend
1863
+ });
1864
+ composer.focus();
1865
+ } else {
1866
+ input.focus();
1867
+ input.setSelectionRange(input.value.length, input.value.length);
1868
+ }
1869
+ save.addEventListener("click", saveEdit);
1870
+ send?.addEventListener("click", saveAndSend);
1871
+ cancel.addEventListener("click", cancelEdit);
1872
+ overlay.addEventListener("click", dismiss);
1873
+ return () => {
1874
+ mounted = false;
1875
+ composer?.dispose();
1876
+ closeConfirmation?.();
1877
+ save.removeEventListener("click", saveEdit);
1878
+ send?.removeEventListener("click", saveAndSend);
1879
+ cancel.removeEventListener("click", cancelEdit);
1880
+ overlay.removeEventListener("click", dismiss);
1881
+ overlay.remove();
1882
+ };
1883
+ }
1884
+ /** Mount one block-picker DOM effect and return its exact inverse. */
1885
+ function mountPicker(blocks, select, close) {
1886
+ const overlay = document.createElement("div");
1887
+ overlay.className = STYLE.overlay;
1888
+ const panel = document.createElement("div");
1889
+ panel.className = STYLE.panel;
1890
+ const title = document.createElement("div");
1891
+ title.className = STYLE.title;
1892
+ title.textContent = blocks.some((block) => block.kind === "user") ? "编辑消息" : "编辑助手消息";
1893
+ const picker = document.createElement("div");
1894
+ picker.className = STYLE.picker;
1895
+ const itemListeners = [];
1896
+ for (const block of blocks) {
1897
+ const item = document.createElement("button");
1898
+ item.className = STYLE.pickerItem;
1899
+ item.textContent = `${blockTitle(block.kind)}:${block.text.slice(0, 24)}${block.text.length > 24 ? "…" : ""}`;
1900
+ const listener = () => {
1901
+ select(block);
1902
+ };
1903
+ item.addEventListener("click", listener);
1904
+ itemListeners.push({
1905
+ item,
1906
+ listener
1907
+ });
1908
+ picker.appendChild(item);
1909
+ }
1910
+ const cancel = document.createElement("button");
1911
+ cancel.textContent = "取消";
1912
+ cancel.className = STYLE.pickerItemActive;
1913
+ const cancelPicker = () => {
1914
+ close();
1915
+ };
1916
+ cancel.addEventListener("click", cancelPicker);
1917
+ panel.append(title, picker, cancel);
1918
+ overlay.appendChild(panel);
1919
+ document.body.appendChild(overlay);
1920
+ return () => {
1921
+ for (const { item, listener } of itemListeners) item.removeEventListener("click", listener);
1922
+ cancel.removeEventListener("click", cancelPicker);
1923
+ overlay.remove();
1924
+ };
1925
+ }
1926
+ /** Compose every overlay with a single idempotent active inverse. */
1927
+ function createOverlayHost(edit, attachmentTools, composerTools) {
1928
+ let active;
1929
+ const mount = (effect) => {
1930
+ active?.();
1931
+ let cleanup = () => {};
1932
+ let mounted = true;
1933
+ const close = () => {
1934
+ if (!mounted) return;
1935
+ mounted = false;
1936
+ cleanup();
1937
+ if (active === close) active = void 0;
1938
+ };
1939
+ active = close;
1940
+ try {
1941
+ cleanup = effect(close);
1942
+ } catch (error) {
1943
+ active = void 0;
1944
+ mounted = false;
1945
+ throw error;
1946
+ }
1947
+ };
1948
+ const editBlock = (block) => {
1949
+ mount((close) => mountEditor(block, edit, close, attachmentTools, composerTools));
1950
+ };
1951
+ const chooseBlock = (blocks) => {
1952
+ mount((close) => mountPicker(blocks, (block) => {
1953
+ close();
1954
+ editBlock(block);
1955
+ }, close));
1956
+ };
1957
+ return {
1958
+ editBlock,
1959
+ chooseBlock,
1960
+ dispose: () => {
1961
+ active?.();
1962
+ }
1963
+ };
1964
+ }
1965
+ /** 按消息标识更新操作按钮,数据刷新时保留按钮与正在编辑的弹窗。 */
1966
+ function InlineMessageEdit({ messages, edit, retry, attachmentTools, composerTools, disabled = false }) {
1967
+ const current = (0, react.useRef)({
1968
+ messages,
1969
+ disabled
1970
+ });
1971
+ current.current = {
1972
+ messages,
1973
+ disabled
1974
+ };
1975
+ const synchronize = (0, react.useRef)(void 0);
1976
+ (0, react.useLayoutEffect)(() => {
1977
+ const bindings = /* @__PURE__ */ new Map();
1978
+ const overlays = createOverlayHost(edit, attachmentTools, composerTools);
1979
+ let observer;
1980
+ let alive = true;
1981
+ let frame;
1982
+ let scheduled = false;
1983
+ const sync = () => {
1984
+ const actionRows = Array.from(document.querySelectorAll("[class*=\"actions\"]"));
1985
+ const presentRows = new Set(actionRows);
1986
+ for (const [row, binding] of bindings) {
1987
+ if (presentRows.has(row)) continue;
1988
+ binding.dispose();
1989
+ bindings.delete(row);
1990
+ }
1991
+ const blocksByEvent = /* @__PURE__ */ new Map();
1992
+ const turns = /* @__PURE__ */ new Map();
1993
+ for (const message of current.current.messages) {
1994
+ let blocks = blocksByEvent.get(message.eventSeq);
1995
+ if (blocks === void 0) {
1996
+ blocks = [];
1997
+ blocksByEvent.set(message.eventSeq, blocks);
1998
+ }
1999
+ blocks.push(message);
2000
+ let turn = turns.get(message.turn);
2001
+ if (turn === void 0) {
2002
+ turn = {};
2003
+ turns.set(message.turn, turn);
2004
+ }
2005
+ if (message.kind === "user") turn.user ??= message.eventSeq;
2006
+ else turn.assistant = message.eventSeq;
2007
+ }
2008
+ const claimedEvents = /* @__PURE__ */ new Set();
2009
+ for (const row of actionRows) {
2010
+ const marker = row;
2011
+ let binding = bindings.get(row);
2012
+ if (marker.__messageEditInjected === true && binding === void 0) {
2013
+ if (marker.__messageEditEventSeq !== void 0) claimedEvents.add(marker.__messageEditEventSeq);
2014
+ continue;
2015
+ }
2016
+ const node = row.closest("[data-chat-flow-kind]");
2017
+ const kind = node?.dataset["chatFlowKind"];
2018
+ const turn = Number(node?.dataset["chatTurn"]);
2019
+ const eventSeq = !Number.isSafeInteger(turn) || turn < 1 ? void 0 : kind === "user" ? turns.get(turn)?.user : kind === "turn-tail" ? turns.get(turn)?.assistant : void 0;
2020
+ const blocks = eventSeq === void 0 ? void 0 : blocksByEvent.get(eventSeq);
2021
+ if (eventSeq === void 0 || blocks === void 0 || claimedEvents.has(eventSeq)) {
2022
+ binding?.dispose();
2023
+ bindings.delete(row);
2024
+ continue;
2025
+ }
2026
+ claimedEvents.add(eventSeq);
2027
+ if (binding?.eventSeq === eventSeq && row.contains(binding.editButton) && row.contains(binding.retryButton)) {
2028
+ binding.blocks = blocks;
2029
+ binding.editButton.disabled = current.current.disabled;
2030
+ binding.retryButton.disabled = current.current.disabled;
2031
+ continue;
2032
+ }
2033
+ binding?.dispose();
2034
+ bindings.delete(row);
2035
+ const previousMarker = marker.__messageEditInjected;
2036
+ const previousEventSeq = marker.__messageEditEventSeq;
2037
+ marker.__messageEditInjected = true;
2038
+ marker.__messageEditEventSeq = eventSeq;
2039
+ const editButton = document.createElement("button");
2040
+ editButton.className = STYLE.iconButton;
2041
+ editButton.setAttribute("aria-label", "编辑消息");
2042
+ editButton.title = "编辑消息";
2043
+ editButton.setAttribute("data-message-edit-control", "");
2044
+ editButton.disabled = current.current.disabled;
2045
+ editButton.appendChild(svgIcon(EDIT_PATH));
2046
+ const editMessage = () => {
2047
+ if (current.current.disabled || binding === void 0) return;
2048
+ if (binding.blocks.length === 1 && binding.blocks[0] !== void 0) overlays.editBlock(binding.blocks[0]);
2049
+ else overlays.chooseBlock(binding.blocks);
2050
+ };
2051
+ editButton.addEventListener("click", editMessage);
2052
+ const retryButton = document.createElement("button");
2053
+ retryButton.className = STYLE.iconButton;
2054
+ retryButton.setAttribute("aria-label", "重试此回合");
2055
+ retryButton.title = "重试此回合";
2056
+ retryButton.setAttribute("data-message-edit-control", "");
2057
+ retryButton.disabled = current.current.disabled;
2058
+ retryButton.appendChild(svgIcon(REFRESH_PATH));
2059
+ const retryTurn = () => {
2060
+ if (current.current.disabled) return;
2061
+ const targetTurn = binding?.blocks[0]?.turn;
2062
+ if (targetTurn !== void 0) retry(targetTurn, "truncate");
2063
+ };
2064
+ retryButton.addEventListener("click", retryTurn);
2065
+ const lastOfficial = Array.from(row.querySelectorAll("button")).filter((button) => button !== editButton && button !== retryButton).at(-1);
2066
+ if (lastOfficial !== void 0) {
2067
+ lastOfficial.insertAdjacentElement("afterend", retryButton);
2068
+ lastOfficial.insertAdjacentElement("afterend", editButton);
2069
+ } else {
2070
+ row.appendChild(editButton);
2071
+ row.appendChild(retryButton);
2072
+ }
2073
+ const dispose = () => {
2074
+ editButton.removeEventListener("click", editMessage);
2075
+ retryButton.removeEventListener("click", retryTurn);
2076
+ editButton.remove();
2077
+ retryButton.remove();
2078
+ if (previousMarker === void 0) delete marker.__messageEditInjected;
2079
+ else marker.__messageEditInjected = previousMarker;
2080
+ if (previousEventSeq === void 0) delete marker.__messageEditEventSeq;
2081
+ else marker.__messageEditEventSeq = previousEventSeq;
2082
+ };
2083
+ binding = {
2084
+ eventSeq,
2085
+ blocks,
2086
+ editButton,
2087
+ retryButton,
2088
+ dispose
2089
+ };
2090
+ bindings.set(row, binding);
2091
+ }
2092
+ };
2093
+ synchronize.current = sync;
2094
+ sync();
2095
+ observer = new MutationObserver(() => {
2096
+ if (!alive || scheduled) return;
2097
+ scheduled = true;
2098
+ frame = requestAnimationFrame(() => {
2099
+ frame = void 0;
2100
+ scheduled = false;
2101
+ if (alive) sync();
2102
+ });
2103
+ });
2104
+ observer.observe(document.body, {
2105
+ childList: true,
2106
+ subtree: true
2107
+ });
2108
+ return () => {
2109
+ alive = false;
2110
+ if (frame !== void 0) cancelAnimationFrame(frame);
2111
+ observer?.disconnect();
2112
+ overlays.dispose();
2113
+ for (const binding of bindings.values()) binding.dispose();
2114
+ bindings.clear();
2115
+ if (synchronize.current === sync) synchronize.current = void 0;
2116
+ };
2117
+ }, [
2118
+ edit,
2119
+ retry,
2120
+ attachmentTools,
2121
+ composerTools
2122
+ ]);
2123
+ (0, react.useLayoutEffect)(() => {
2124
+ synchronize.current?.();
2125
+ }, [messages, disabled]);
2126
+ return null;
2127
+ }
2128
+ //#endregion
2129
+ //#region \0dsh-css:D:\ai\home\codex\dsh-message-edit\src\client\MessageEditHeader.module.css.mjs
2130
+ const css$1 = ".g0PbfG_root{align-items:center;gap:4px;display:inline-flex}.g0PbfG_iconButton,.g0PbfG_rerollButton{box-sizing:border-box;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border:0}.g0PbfG_iconButton{border-radius:50%;justify-content:center;align-items:center;width:28px;height:28px;font-size:16px;line-height:20px;display:inline-flex}.g0PbfG_rerollButton{border:1px solid var(--dsw-alias-border-l2);border-radius:14px;height:28px;padding:0 10px;font-size:12px;line-height:18px}.g0PbfG_iconButton:hover:not(:disabled),.g0PbfG_rerollButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.g0PbfG_iconButton:focus-visible,.g0PbfG_rerollButton:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.g0PbfG_iconButton:disabled,.g0PbfG_rerollButton:disabled{cursor:default;opacity:.4}.g0PbfG_counter{min-width:108px;color:var(--dsw-alias-label-tertiary);text-align:center;font-size:11px;line-height:18px}@media (width<=760px){.g0PbfG_counter{display:none}}";
2131
+ const tagId$1 = "@sh1robana/dsh-plugin-message-edit/MessageEditHeader.module.css";
2132
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
2133
+ const tag = document.createElement("style");
2134
+ tag.dataset.plugin = "@sh1robana/dsh-plugin-message-edit";
2135
+ tag.dataset.pluginCss = tagId$1;
2136
+ tag.textContent = css$1;
2137
+ document.head.appendChild(tag);
2138
+ }
2139
+ var MessageEditHeader_module_css_default = {
2140
+ "root": "g0PbfG_root",
2141
+ "counter": "g0PbfG_counter",
2142
+ "iconButton": "g0PbfG_iconButton",
2143
+ "rerollButton": "g0PbfG_rerollButton"
2144
+ };
2145
+ //#endregion
2146
+ //#region src/client/MessageEditHeader.tsx
2147
+ /** Header contribution shared with the Timeline controller. */
2148
+ function MessageEditHeader({ useMessageEdit, acquire, load, openVersion, reroll, edit, retry, attachmentTools, composerTools }) {
2149
+ const state = useMessageEdit((value) => value);
2150
+ (0, react.useEffect)(() => {
2151
+ const release = acquire();
2152
+ load();
2153
+ return release;
2154
+ }, [acquire, load]);
2155
+ const timeline = state.timeline;
2156
+ const versions = state.timeline?.versions ?? [];
2157
+ const undoSessionId = timeline?.undoStack[0];
2158
+ const redoSessionId = timeline?.redoSessionIds.at(-1);
2159
+ const effectDepth = timeline?.undoStack.length ?? 0;
2160
+ const busy = state.pending !== null || state.status !== "ready";
2161
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InlineMessageEdit, {
2162
+ messages: timeline?.messages ?? [],
2163
+ edit,
2164
+ retry,
2165
+ attachmentTools,
2166
+ composerTools,
2167
+ disabled: busy
2168
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2169
+ className: MessageEditHeader_module_css_default["root"],
2170
+ children: [
2171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2172
+ type: "button",
2173
+ className: MessageEditHeader_module_css_default["iconButton"],
2174
+ "aria-label": "撤销当前版本效果",
2175
+ title: "撤销当前效果,保留更早效果",
2176
+ disabled: undoSessionId === void 0 || busy,
2177
+ onClick: () => {
2178
+ if (undoSessionId !== void 0) openVersion(undoSessionId);
2179
+ },
2180
+ children: "←"
2181
+ }),
2182
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2183
+ className: MessageEditHeader_module_css_default["counter"],
2184
+ children: versions.length === 0 ? "效果 —" : `效果 ${String(effectDepth)} 层 · ${String(versions.length)} 版`
2185
+ }),
2186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2187
+ type: "button",
2188
+ className: MessageEditHeader_module_css_default["iconButton"],
2189
+ "aria-label": "重施加下一版本效果",
2190
+ title: timeline !== null && timeline.redoSessionIds.length > 1 ? `重施加最新效果(另有 ${String(timeline.redoSessionIds.length - 1)} 个分支)` : "重施加下一效果",
2191
+ disabled: redoSessionId === void 0 || busy,
2192
+ onClick: () => {
2193
+ if (redoSessionId !== void 0) openVersion(redoSessionId);
2194
+ },
2195
+ children: "→"
2196
+ }),
2197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2198
+ type: "button",
2199
+ className: MessageEditHeader_module_css_default["rerollButton"],
2200
+ disabled: busy || state.timeline === null,
2201
+ onClick: () => {
2202
+ reroll();
2203
+ },
2204
+ children: state.pending === "reroll" ? "正在重生成…" : "重生成"
2205
+ })
2206
+ ]
2207
+ })] });
2208
+ }
2209
+ //#endregion
2210
+ //#region \0dsh-css:D:\ai\home\codex\dsh-message-edit\src\client\MessageEditTimelineView.module.css.mjs
2211
+ const css = ".Si12YW_root{box-sizing:border-box;width:100%;height:100%;min-height:0;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);padding:24px;overflow:auto}.Si12YW_pageHeader{justify-content:space-between;align-items:flex-start;gap:20px;max-width:1480px;margin:0 auto 16px;display:flex}.Si12YW_title,.Si12YW_intro,.Si12YW_subtitle,.Si12YW_notice,.Si12YW_error,.Si12YW_empty,.Si12YW_turnTitle,.Si12YW_turnPreview,.Si12YW_messageText{margin:0}.Si12YW_title{font-size:22px;font-weight:600;line-height:30px}.Si12YW_intro{max-width:700px;color:var(--dsw-alias-label-tertiary);margin-top:4px;font-size:13px;line-height:20px}.Si12YW_headerActions{flex:none;align-items:flex-end;gap:8px;display:flex}.Si12YW_cascadeField{color:var(--dsw-alias-label-secondary);flex-direction:column;gap:4px;font-size:11px;line-height:16px;display:flex}.Si12YW_select,.Si12YW_textarea,.Si12YW_primaryButton,.Si12YW_secondaryButton,.Si12YW_textButton,.Si12YW_versionButton{box-sizing:border-box;font:inherit}.Si12YW_select,.Si12YW_textarea{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border-radius:8px}.Si12YW_select{height:34px;padding:0 30px 0 9px;font-size:12px}.Si12YW_primaryButton,.Si12YW_secondaryButton,.Si12YW_textButton,.Si12YW_versionButton{cursor:pointer;border:0}.Si12YW_primaryButton,.Si12YW_secondaryButton{border-radius:17px;justify-content:center;align-items:center;min-height:34px;padding:0 13px;font-size:12px;line-height:18px;display:inline-flex}.Si12YW_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-primary-fill)}.Si12YW_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.Si12YW_secondaryButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Si12YW_secondaryButton:hover:not(:disabled),.Si12YW_textButton:hover:not(:disabled),.Si12YW_versionButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.Si12YW_primaryButton:disabled,.Si12YW_secondaryButton:disabled,.Si12YW_textButton:disabled,.Si12YW_versionButton:disabled,.Si12YW_select:disabled{cursor:default;opacity:.45}.Si12YW_primaryButton:focus-visible,.Si12YW_secondaryButton:focus-visible,.Si12YW_textButton:focus-visible,.Si12YW_versionButton:focus-visible,.Si12YW_select:focus-visible,.Si12YW_textarea:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.Si12YW_notice,.Si12YW_error{max-width:1480px;margin:0 auto 10px;font-size:12px;line-height:18px}.Si12YW_notice{color:var(--dsw-alias-state-warn-label)}.Si12YW_error{color:var(--dsw-alias-state-error-primary)}.Si12YW_status{box-sizing:border-box;width:100%;height:100%;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);flex-direction:column;align-items:flex-start;gap:12px;padding:24px;display:flex}.Si12YW_status .Si12YW_error{margin:0}.Si12YW_columns{grid-template-columns:minmax(280px,.72fr) minmax(520px,1.75fr);align-items:start;gap:18px;max-width:1480px;margin:0 auto;display:grid}.Si12YW_versionsPanel,.Si12YW_turnsPanel{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;min-width:0;padding:16px}.Si12YW_versionsPanel{position:sticky;top:0}.Si12YW_sectionHeading{justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;display:flex}.Si12YW_effectControls{background:var(--dsw-alias-bg-module-platform);border-radius:9px;flex-direction:column;gap:8px;margin-bottom:12px;padding:10px;display:flex}.Si12YW_effectDepth{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.Si12YW_effectButtons{flex-wrap:wrap;gap:6px;display:flex}.Si12YW_effectButtons .Si12YW_secondaryButton{min-height:28px;padding:0 10px;font-size:11px}.Si12YW_subtitle{font-size:16px;font-weight:500;line-height:24px}.Si12YW_count{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.Si12YW_versionList,.Si12YW_turnList{margin:0;padding:0;list-style:none}.Si12YW_versionList{flex-direction:column;gap:4px;display:flex}.Si12YW_versionItem{--message-edit-depth:0;padding-left:calc(var(--message-edit-depth) * 14px);position:relative}.Si12YW_versionButton{width:100%;min-width:0;color:var(--dsw-alias-label-secondary);text-align:left;background:0 0;border-radius:9px;align-items:flex-start;gap:9px;padding:9px;display:flex;position:relative}.Si12YW_versionButton[data-current]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-module-platform);opacity:1}.Si12YW_versionButton:not([data-current]) .Si12YW_pathBadge{opacity:.8}.Si12YW_versionLine{background:var(--dsw-alias-border-l2);width:1px;position:absolute;top:0;bottom:0;left:14px}.Si12YW_versionDot{z-index:1;border:2px solid var(--dsw-alias-bg-layer-1);background:var(--dsw-alias-label-tertiary);border-radius:50%;flex:none;width:7px;height:7px;margin-top:6px}.Si12YW_versionButton[data-current] .Si12YW_versionDot{border-color:var(--dsw-alias-bg-module-platform);background:var(--dsw-alias-brand-primary)}.Si12YW_versionMain{flex-direction:column;flex:1;min-width:0;display:flex}.Si12YW_versionTitle{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.Si12YW_versionMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10px;line-height:16px;overflow:hidden}.Si12YW_versionDiff{color:var(--dsw-alias-label-tertiary);flex-direction:column;gap:2px;margin-top:5px;font-size:10px;line-height:15px;display:flex}.Si12YW_versionDiff span{-webkit-line-clamp:2;white-space:pre-wrap;overflow-wrap:anywhere;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.Si12YW_currentBadge,.Si12YW_pathBadge,.Si12YW_kindBadge{border-radius:9px;flex:none;padding:1px 6px;font-size:10px;line-height:17px}.Si12YW_currentBadge{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-1)}.Si12YW_pathBadge{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-1)}.Si12YW_turnList{flex-direction:column;gap:14px;display:flex}.Si12YW_turnSection{border:1px solid var(--dsw-alias-border-l2);border-radius:11px;padding:13px}.Si12YW_turnHeader,.Si12YW_messageHeader,.Si12YW_editorActions{justify-content:space-between;align-items:center;gap:10px;display:flex}.Si12YW_turnHeader{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:flex-start;padding-bottom:11px}.Si12YW_turnTitle{font-size:14px;font-weight:500;line-height:22px}.Si12YW_turnPreview{max-width:700px;color:var(--dsw-alias-label-tertiary);-webkit-line-clamp:2;white-space:pre-wrap;-webkit-box-orient:vertical;font-size:11px;line-height:17px;display:-webkit-box;overflow:hidden}.Si12YW_messageList{flex-direction:column;gap:8px;margin-top:10px;display:flex}.Si12YW_messageCard{background:var(--dsw-alias-bg-module-platform);border-radius:9px;padding:10px}.Si12YW_messageHeader{justify-content:flex-start}.Si12YW_kindBadge{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1)}.Si12YW_kindBadge[data-kind=assistant\\.reasoning]{color:var(--dsw-alias-label-tertiary)}.Si12YW_messageTime{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:17px}.Si12YW_textButton{color:var(--dsw-alias-label-secondary);background:0 0;border-radius:12px;margin-left:auto;padding:3px 8px;font-size:11px;line-height:17px}.Si12YW_messageText{max-height:220px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere;margin-top:7px;font-family:inherit;font-size:12px;line-height:19px;overflow:auto}.Si12YW_editor{margin-top:8px}.Si12YW_textarea{resize:vertical;width:100%;min-height:120px;padding:9px;font-size:12px;line-height:19px}.Si12YW_editorActions{margin-top:8px}.Si12YW_editorHint{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:16px}.Si12YW_empty{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-module-platform);border-radius:10px;padding:18px;font-size:13px;line-height:20px}@media (width<=1000px){.Si12YW_columns{grid-template-columns:1fr}.Si12YW_versionsPanel{position:static}}@media (width<=680px){.Si12YW_root{padding:16px}.Si12YW_pageHeader,.Si12YW_headerActions,.Si12YW_turnHeader,.Si12YW_editorActions{flex-direction:column;align-items:stretch}.Si12YW_headerActions,.Si12YW_primaryButton,.Si12YW_secondaryButton{width:100%}}";
2212
+ const tagId = "@sh1robana/dsh-plugin-message-edit/MessageEditTimelineView.module.css";
2213
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
2214
+ const tag = document.createElement("style");
2215
+ tag.dataset.plugin = "@sh1robana/dsh-plugin-message-edit";
2216
+ tag.dataset.pluginCss = tagId;
2217
+ tag.textContent = css;
2218
+ document.head.appendChild(tag);
2219
+ }
2220
+ var MessageEditTimelineView_module_css_default = {
2221
+ "versionItem": "Si12YW_versionItem",
2222
+ "error": "Si12YW_error",
2223
+ "versionButton": "Si12YW_versionButton",
2224
+ "secondaryButton": "Si12YW_secondaryButton",
2225
+ "versionsPanel": "Si12YW_versionsPanel",
2226
+ "headerActions": "Si12YW_headerActions",
2227
+ "textButton": "Si12YW_textButton",
2228
+ "versionTitle": "Si12YW_versionTitle",
2229
+ "pathBadge": "Si12YW_pathBadge",
2230
+ "versionDot": "Si12YW_versionDot",
2231
+ "versionList": "Si12YW_versionList",
2232
+ "kindBadge": "Si12YW_kindBadge",
2233
+ "messageCard": "Si12YW_messageCard",
2234
+ "turnHeader": "Si12YW_turnHeader",
2235
+ "turnPreview": "Si12YW_turnPreview",
2236
+ "effectDepth": "Si12YW_effectDepth",
2237
+ "messageTime": "Si12YW_messageTime",
2238
+ "effectButtons": "Si12YW_effectButtons",
2239
+ "empty": "Si12YW_empty",
2240
+ "messageList": "Si12YW_messageList",
2241
+ "root": "Si12YW_root",
2242
+ "versionLine": "Si12YW_versionLine",
2243
+ "textarea": "Si12YW_textarea",
2244
+ "select": "Si12YW_select",
2245
+ "count": "Si12YW_count",
2246
+ "cascadeField": "Si12YW_cascadeField",
2247
+ "status": "Si12YW_status",
2248
+ "turnTitle": "Si12YW_turnTitle",
2249
+ "columns": "Si12YW_columns",
2250
+ "turnSection": "Si12YW_turnSection",
2251
+ "editorActions": "Si12YW_editorActions",
2252
+ "editorHint": "Si12YW_editorHint",
2253
+ "intro": "Si12YW_intro",
2254
+ "sectionHeading": "Si12YW_sectionHeading",
2255
+ "turnList": "Si12YW_turnList",
2256
+ "currentBadge": "Si12YW_currentBadge",
2257
+ "messageHeader": "Si12YW_messageHeader",
2258
+ "title": "Si12YW_title",
2259
+ "turnsPanel": "Si12YW_turnsPanel",
2260
+ "effectControls": "Si12YW_effectControls",
2261
+ "primaryButton": "Si12YW_primaryButton",
2262
+ "versionDiff": "Si12YW_versionDiff",
2263
+ "editor": "Si12YW_editor",
2264
+ "messageText": "Si12YW_messageText",
2265
+ "subtitle": "Si12YW_subtitle",
2266
+ "versionMain": "Si12YW_versionMain",
2267
+ "notice": "Si12YW_notice",
2268
+ "pageHeader": "Si12YW_pageHeader",
2269
+ "versionMeta": "Si12YW_versionMeta"
2270
+ };
2271
+ //#endregion
2272
+ //#region src/client/MessageEditTimelineView.tsx
2273
+ /** Timeline tab: durable version tree plus turn/block edit and retry controls. */
2274
+ const BLOCK_LABEL = {
2275
+ user: "用户消息",
2276
+ "assistant.reasoning": "助手思考",
2277
+ "assistant.response": "助手回复"
2278
+ };
2279
+ const OPERATION_LABEL = {
2280
+ edit: "编辑",
2281
+ reroll: "重生成",
2282
+ retry: "重试"
2283
+ };
2284
+ function timeLabel(value) {
2285
+ return new Date(value).toLocaleString("zh-CN", {
2286
+ month: "2-digit",
2287
+ day: "2-digit",
2288
+ hour: "2-digit",
2289
+ minute: "2-digit",
2290
+ second: "2-digit"
2291
+ });
2292
+ }
2293
+ function turnSections(turns, messages) {
2294
+ return turns.map((retry) => ({
2295
+ retry,
2296
+ messages: messages.filter((message) => message.turn === retry.turn)
2297
+ }));
2298
+ }
2299
+ function VersionRow({ version, disabled, onOpen }) {
2300
+ const depthStyle = { "--message-edit-depth": String(version.depth) };
2301
+ const operation = version.operation === void 0 ? version.parentSessionId === void 0 ? "原始版本" : "外部分支" : OPERATION_LABEL[version.operation];
2302
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
2303
+ className: MessageEditTimelineView_module_css_default["versionItem"],
2304
+ style: depthStyle,
2305
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2306
+ type: "button",
2307
+ className: MessageEditTimelineView_module_css_default["versionButton"],
2308
+ "data-current": version.current || void 0,
2309
+ disabled: version.current || disabled,
2310
+ onClick: () => {
2311
+ onOpen(version.sessionId);
2312
+ },
2313
+ children: [
2314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2315
+ className: MessageEditTimelineView_module_css_default["versionLine"],
2316
+ "aria-hidden": true
2317
+ }),
2318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2319
+ className: MessageEditTimelineView_module_css_default["versionDot"],
2320
+ "aria-hidden": true
2321
+ }),
2322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2323
+ className: MessageEditTimelineView_module_css_default["versionMain"],
2324
+ children: [
2325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2326
+ className: MessageEditTimelineView_module_css_default["versionTitle"],
2327
+ children: [operation, version.targetTurn === void 0 ? null : ` · 回合 ${String(version.targetTurn)}`]
2328
+ }),
2329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2330
+ className: MessageEditTimelineView_module_css_default["versionMeta"],
2331
+ children: [
2332
+ timeLabel(version.createdAt),
2333
+ " · ",
2334
+ version.sessionId.slice(0, 12)
2335
+ ]
2336
+ }),
2337
+ version.before === void 0 && version.after === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2338
+ className: MessageEditTimelineView_module_css_default["versionDiff"],
2339
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["原:", version.before || "(空)"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["新:", version.after || "(空)"] })]
2340
+ })
2341
+ ]
2342
+ }),
2343
+ version.current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2344
+ className: MessageEditTimelineView_module_css_default["currentBadge"],
2345
+ children: "当前"
2346
+ }) : version.onCurrentEffectPath ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2347
+ className: MessageEditTimelineView_module_css_default["pathBadge"],
2348
+ children: "链上"
2349
+ }) : null
2350
+ ]
2351
+ })
2352
+ });
2353
+ }
2354
+ function MessageCard({ message, disabled, onBeginEdit }) {
2355
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
2356
+ className: MessageEditTimelineView_module_css_default["messageCard"],
2357
+ children: [
2358
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2359
+ className: MessageEditTimelineView_module_css_default["messageHeader"],
2360
+ children: [
2361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2362
+ className: MessageEditTimelineView_module_css_default["kindBadge"],
2363
+ "data-kind": message.kind,
2364
+ children: BLOCK_LABEL[message.kind]
2365
+ }),
2366
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2367
+ className: MessageEditTimelineView_module_css_default["messageTime"],
2368
+ children: timeLabel(message.time)
2369
+ }),
2370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2371
+ type: "button",
2372
+ className: MessageEditTimelineView_module_css_default["textButton"],
2373
+ disabled,
2374
+ onClick: () => onBeginEdit(message),
2375
+ children: "编辑"
2376
+ })
2377
+ ]
2378
+ }),
2379
+ message.attachments?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2380
+ className: MessageEditTimelineView_module_css_default["messageTime"],
2381
+ children: ["附件:", message.attachments.map((item) => item.content.attachment.name ?? "图片附件").join("、")]
2382
+ }) : null,
2383
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
2384
+ className: MessageEditTimelineView_module_css_default["messageText"],
2385
+ children: message.text || "(空正文)"
2386
+ })
2387
+ ]
2388
+ });
2389
+ }
2390
+ /** Conversation-view entry point. */
2391
+ function MessageEditTimelineView({ useMessageEdit, acquire, load, edit, retry, reroll, openVersion, attachmentTools, composerTools }) {
2392
+ const state = useMessageEdit((value) => value);
2393
+ const [cascade, setCascade] = (0, react.useState)("truncate");
2394
+ const [editing, setEditing] = (0, react.useState)(null);
2395
+ (0, react.useEffect)(() => {
2396
+ const release = acquire();
2397
+ load();
2398
+ return release;
2399
+ }, [acquire, load]);
2400
+ const timeline = state.timeline;
2401
+ const sections = (0, react.useMemo)(() => timeline === null ? [] : turnSections(timeline.retryableTurns, timeline.messages), [timeline]);
2402
+ const busy = state.pending !== null || state.status !== "ready";
2403
+ (0, react.useEffect)(() => {
2404
+ setEditing((current) => {
2405
+ if (current === null || timeline === null) return current;
2406
+ return timeline.messages.some((message) => message.key === current.message.key) ? current : null;
2407
+ });
2408
+ }, [timeline]);
2409
+ const cascadeRef = (0, react.useRef)(cascade);
2410
+ cascadeRef.current = cascade;
2411
+ (0, react.useEffect)(() => {
2412
+ if (editing === null) return;
2413
+ return mountEditor(editing.message, (message, text, _policy, regenerate, attachments, settings) => edit(message, text, cascadeRef.current, regenerate, attachments, settings), () => setEditing(null), attachmentTools, composerTools);
2414
+ }, [
2415
+ editing,
2416
+ edit,
2417
+ attachmentTools,
2418
+ composerTools
2419
+ ]);
2420
+ if (timeline === null && (state.status === "idle" || state.status === "loading")) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2421
+ className: MessageEditTimelineView_module_css_default["status"],
2422
+ children: "正在载入消息时间线…"
2423
+ });
2424
+ if (timeline === null && state.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2425
+ className: MessageEditTimelineView_module_css_default["status"],
2426
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2427
+ className: MessageEditTimelineView_module_css_default["error"],
2428
+ children: state.error
2429
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2430
+ type: "button",
2431
+ className: MessageEditTimelineView_module_css_default["secondaryButton"],
2432
+ onClick: load,
2433
+ children: "重新载入"
2434
+ })]
2435
+ });
2436
+ if (timeline === null) return null;
2437
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2438
+ className: MessageEditTimelineView_module_css_default["root"],
2439
+ children: [
2440
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2441
+ className: MessageEditTimelineView_module_css_default["pageHeader"],
2442
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
2443
+ className: MessageEditTimelineView_module_css_default["title"],
2444
+ children: "消息编辑与重生成"
2445
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2446
+ className: MessageEditTimelineView_module_css_default["intro"],
2447
+ children: "每次修改都会与其恢复版本成对记录;回合及其完整工具链作为一个整体重新计算。"
2448
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2449
+ className: MessageEditTimelineView_module_css_default["headerActions"],
2450
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2451
+ className: MessageEditTimelineView_module_css_default["cascadeField"],
2452
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "后续策略" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2453
+ className: MessageEditTimelineView_module_css_default["select"],
2454
+ value: cascade,
2455
+ disabled: busy,
2456
+ onChange: (event) => {
2457
+ setCascade(event.currentTarget.value);
2458
+ },
2459
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2460
+ value: "truncate",
2461
+ children: "截断后续(默认)"
2462
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2463
+ value: "preserve",
2464
+ children: "保留输入并重生成后续"
2465
+ })]
2466
+ })]
2467
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2468
+ type: "button",
2469
+ className: MessageEditTimelineView_module_css_default["primaryButton"],
2470
+ disabled: busy,
2471
+ onClick: () => {
2472
+ reroll();
2473
+ },
2474
+ children: state.pending === "reroll" ? "正在重生成…" : "重生成最后回复"
2475
+ })]
2476
+ })]
2477
+ }),
2478
+ state.error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2479
+ className: MessageEditTimelineView_module_css_default["error"],
2480
+ children: state.error
2481
+ }),
2482
+ state.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2483
+ className: MessageEditTimelineView_module_css_default["notice"],
2484
+ children: "正在刷新时间线…"
2485
+ }) : null,
2486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2487
+ className: MessageEditTimelineView_module_css_default["columns"],
2488
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
2489
+ className: MessageEditTimelineView_module_css_default["versionsPanel"],
2490
+ "aria-label": "版本时间线",
2491
+ children: [
2492
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2493
+ className: MessageEditTimelineView_module_css_default["sectionHeading"],
2494
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2495
+ className: MessageEditTimelineView_module_css_default["subtitle"],
2496
+ children: "版本时间线"
2497
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2498
+ className: MessageEditTimelineView_module_css_default["count"],
2499
+ children: String(timeline.versions.length)
2500
+ })]
2501
+ }),
2502
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2503
+ className: MessageEditTimelineView_module_css_default["effectControls"],
2504
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2505
+ className: MessageEditTimelineView_module_css_default["effectDepth"],
2506
+ children: [
2507
+ "当前效果链 ",
2508
+ String(timeline.undoStack.length),
2509
+ " 层"
2510
+ ]
2511
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2512
+ className: MessageEditTimelineView_module_css_default["effectButtons"],
2513
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2514
+ type: "button",
2515
+ className: MessageEditTimelineView_module_css_default["secondaryButton"],
2516
+ disabled: busy || timeline.undoStack[0] === void 0,
2517
+ onClick: () => {
2518
+ const target = timeline.undoStack[0];
2519
+ if (target !== void 0) openVersion(target);
2520
+ },
2521
+ children: "撤销当前效果"
2522
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2523
+ type: "button",
2524
+ className: MessageEditTimelineView_module_css_default["secondaryButton"],
2525
+ disabled: busy || timeline.redoSessionIds.length === 0,
2526
+ onClick: () => {
2527
+ const target = timeline.redoSessionIds.at(-1);
2528
+ if (target !== void 0) openVersion(target);
2529
+ },
2530
+ children: timeline.redoSessionIds.length > 1 ? `重施加最新分支(${String(timeline.redoSessionIds.length)})` : "重施加下一效果"
2531
+ })]
2532
+ })]
2533
+ }),
2534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
2535
+ className: MessageEditTimelineView_module_css_default["versionList"],
2536
+ children: timeline.versions.map((version) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VersionRow, {
2537
+ version,
2538
+ disabled: busy,
2539
+ onOpen: (sessionId) => {
2540
+ openVersion(sessionId);
2541
+ }
2542
+ }, version.sessionId))
2543
+ })
2544
+ ]
2545
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
2546
+ className: MessageEditTimelineView_module_css_default["turnsPanel"],
2547
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2548
+ className: MessageEditTimelineView_module_css_default["sectionHeading"],
2549
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2550
+ className: MessageEditTimelineView_module_css_default["subtitle"],
2551
+ children: "已落定消息"
2552
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2553
+ className: MessageEditTimelineView_module_css_default["count"],
2554
+ children: String(timeline.messages.length)
2555
+ })]
2556
+ }), sections.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2557
+ className: MessageEditTimelineView_module_css_default["empty"],
2558
+ children: "当前会话还没有可编辑的已落定回合。"
2559
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
2560
+ className: MessageEditTimelineView_module_css_default["turnList"],
2561
+ children: sections.map((section) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2562
+ className: MessageEditTimelineView_module_css_default["turnSection"],
2563
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2564
+ className: MessageEditTimelineView_module_css_default["turnHeader"],
2565
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
2566
+ className: MessageEditTimelineView_module_css_default["turnTitle"],
2567
+ children: ["回合 ", String(section.retry.turn)]
2568
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2569
+ className: MessageEditTimelineView_module_css_default["turnPreview"],
2570
+ children: section.retry.preview || "(空用户输入)"
2571
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2572
+ type: "button",
2573
+ className: MessageEditTimelineView_module_css_default["secondaryButton"],
2574
+ disabled: busy,
2575
+ onClick: () => {
2576
+ retry(section.retry.turn, cascade);
2577
+ },
2578
+ children: state.pending === "retry" ? "正在重试…" : "重试此回合"
2579
+ })]
2580
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2581
+ className: MessageEditTimelineView_module_css_default["messageList"],
2582
+ children: section.messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageCard, {
2583
+ message,
2584
+ disabled: busy,
2585
+ onBeginEdit: (value) => {
2586
+ setEditing({ message: value });
2587
+ }
2588
+ }, message.key))
2589
+ })]
2590
+ }, section.retry.turn))
2591
+ })]
2592
+ })]
2593
+ })
2594
+ ]
2595
+ });
2596
+ }
2597
+ //#endregion
2598
+ //#region src/client/UserMessageProjection.tsx
2599
+ const CHAT_NODE_SLOT = "conversation.chat.node";
2600
+ const USER_NODE_KEYS = ["user", "steering"];
2601
+ function equalContentValue(left, right) {
2602
+ if (left === right) return true;
2603
+ if (left === null || right === null || typeof left !== "object" || typeof right !== "object") return false;
2604
+ if (Array.isArray(left) !== Array.isArray(right)) return false;
2605
+ if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => equalContentValue(value, right[index]));
2606
+ const leftRecord = left;
2607
+ const rightRecord = right;
2608
+ const keys = Object.keys(leftRecord);
2609
+ return keys.length === Object.keys(rightRecord).length && keys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key) && equalContentValue(leftRecord[key], rightRecord[key]));
2610
+ }
2611
+ function projectedRenderer(original) {
2612
+ const Original = original.component;
2613
+ return function UserMessageProjection(props) {
2614
+ const { acquire, load, node, useMessageEdit } = props;
2615
+ const messages = useMessageEdit((state) => state.timeline?.messages);
2616
+ (0, react.useEffect)(() => {
2617
+ const release = acquire();
2618
+ load();
2619
+ return release;
2620
+ }, [acquire, load]);
2621
+ const savedContent = messages?.find((message) => message.kind === "user" && message.eventSeq === node.data.seq && message.content !== void 0)?.content;
2622
+ let projected = node;
2623
+ if (savedContent !== void 0) {
2624
+ if (!equalContentValue(node.data.content, savedContent)) projected = {
2625
+ ...node,
2626
+ data: {
2627
+ ...node.data,
2628
+ content: savedContent
2629
+ }
2630
+ };
2631
+ } else {
2632
+ let changed = false;
2633
+ const content = node.data.content.map((block, blockIndex) => {
2634
+ if (block.type !== "text") return block;
2635
+ const saved = messages?.find((message) => message.kind === "user" && message.eventSeq === node.data.seq && message.blockIndex === blockIndex);
2636
+ if (saved === void 0 || saved.text === block.text) return block;
2637
+ changed = true;
2638
+ return {
2639
+ ...block,
2640
+ text: saved.text
2641
+ };
2642
+ });
2643
+ if (changed) projected = {
2644
+ ...node,
2645
+ data: {
2646
+ ...node.data,
2647
+ content
2648
+ }
2649
+ };
2650
+ }
2651
+ return (0, react.createElement)(Original, {
2652
+ ...props,
2653
+ node: projected
2654
+ });
2655
+ };
2656
+ }
2657
+ /** 覆写用户消息的显示内容,复用官方气泡、附件和复制操作,不修改宿主的只读事件源。 */
2658
+ function registerUserMessageProjection(ctx, faceFor) {
2659
+ const slots = ctx.slots;
2660
+ if (typeof slots.entries !== "function" || typeof slots.subscribe !== "function") return;
2661
+ ctx.effect(() => {
2662
+ const active = /* @__PURE__ */ new Map();
2663
+ let disposed = false;
2664
+ const sync = () => {
2665
+ if (disposed) return;
2666
+ for (const key of USER_NODE_KEYS) {
2667
+ const current = active.get(key);
2668
+ const original = slots.entries(CHAT_NODE_SLOT).find((entry) => entry.options.key === key && !Array.from(active.values()).some((wrapper) => wrapper.component === entry.component));
2669
+ if (current?.original === original) continue;
2670
+ current?.dispose();
2671
+ active.delete(key);
2672
+ if (original === void 0) continue;
2673
+ const component = projectedRenderer(original);
2674
+ const dispose = slots.register({
2675
+ name: CHAT_NODE_SLOT,
2676
+ key,
2677
+ priority: (original.options.priority ?? 0) - 1,
2678
+ ...original.locale === void 0 ? {} : { locale: original.locale },
2679
+ inject: faceFor
2680
+ }, component);
2681
+ active.set(key, {
2682
+ original,
2683
+ component,
2684
+ dispose
2685
+ });
2686
+ }
2687
+ };
2688
+ const unsubscribe = slots.subscribe(CHAT_NODE_SLOT, sync);
2689
+ sync();
2690
+ return () => {
2691
+ disposed = true;
2692
+ unsubscribe();
2693
+ for (const wrapper of active.values()) wrapper.dispose();
2694
+ active.clear();
2695
+ };
2696
+ }, "message-edit: 用户消息显示投影");
2697
+ }
2698
+ //#endregion
2699
+ //#region src/client/index.ts
2700
+ /** Explicit value sources and slot declaration-order edges. */
2701
+ const inject = [
2702
+ "slots",
2703
+ "uiConversation",
2704
+ "conversation",
2705
+ "uiWorkspace",
2706
+ "connection",
2707
+ "sessions",
2708
+ "commandUi",
2709
+ "remote.commands",
2710
+ "remote.sessionReferenceResolver"
2711
+ ];
2712
+ /** Register both UI contributions over one per-session controller identity. */
2713
+ function apply(ctx) {
2714
+ const globals = globalThis;
2715
+ const diagnostics = Object.freeze({
2716
+ build: MESSAGE_EDIT_BUILD_INFO,
2717
+ diagnostics: async () => {
2718
+ const response = await hostFetch(`${MESSAGE_EDIT_PATH}?view=diagnostics`, {
2719
+ method: "GET",
2720
+ headers: { accept: "application/json" },
2721
+ cache: "no-store"
2722
+ });
2723
+ if (!response.ok) throw new Error(`无法读取插件诊断:HTTP ${String(response.status)}`);
2724
+ const host = await response.json();
2725
+ return {
2726
+ client: MESSAGE_EDIT_BUILD_INFO,
2727
+ host,
2728
+ matched: host.build?.version === MESSAGE_EDIT_BUILD_INFO.version && host.build?.buildId === MESSAGE_EDIT_BUILD_INFO.buildId
2729
+ };
2730
+ }
2731
+ });
2732
+ ctx.effect(() => {
2733
+ const previous = globals.__DSH_MESSAGE_EDIT__;
2734
+ globals.__DSH_MESSAGE_EDIT__ = diagnostics;
2735
+ return () => {
2736
+ if (globals.__DSH_MESSAGE_EDIT__ === diagnostics) if (previous === void 0) delete globals.__DSH_MESSAGE_EDIT__;
2737
+ else globals.__DSH_MESSAGE_EDIT__ = previous;
2738
+ };
2739
+ }, "message-edit: 构建诊断");
2740
+ const controllers = /* @__PURE__ */ new Map();
2741
+ const controllerFor = (sessionId) => {
2742
+ let controller = controllers.get(sessionId);
2743
+ if (controller === void 0) {
2744
+ controller = new MessageEditController(ctx, sessionId);
2745
+ controllers.set(sessionId, controller);
2746
+ }
2747
+ return controller;
2748
+ };
2749
+ registerUserMessageProjection(ctx, (sessionId) => controllerFor(sessionId).face);
2750
+ ctx.on("connection/reset", () => {
2751
+ for (const controller of controllers.values()) controller.refreshIfLoaded();
2752
+ });
2753
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
2754
+ name: "conversation.view",
2755
+ id: "message-edit-timeline",
2756
+ order: 15,
2757
+ label: "Timeline",
2758
+ inject: (sessionId) => controllerFor(sessionId).face
2759
+ }, MessageEditTimelineView));
2760
+ ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
2761
+ name: "conversation.session.header.actions",
2762
+ id: "message-edit-controls",
2763
+ order: 15,
2764
+ inject: (sessionId) => controllerFor(sessionId).face
2765
+ }, MessageEditHeader));
2766
+ }
2767
+ //#endregion
2768
+ exports.MESSAGE_EDIT_BUILD_INFO = MESSAGE_EDIT_BUILD_INFO;
2769
+ exports.apply = apply;
2770
+ exports.inject = inject;
2771
+ return module.exports;
2772
+ }
2773
+ });
2774
+
2775
+ //# sourceMappingURL=client.js.map