@1outres/dsh-plugin-edit-message 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1outres
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,88 @@
1
- # Temporary Holding Version
1
+ # @1outres/dsh-plugin-edit-message
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Edit a message you already sent in the DeepSeek Harness Web UI, the way chat
4
+ apps do: click **Edit** beside Copy, change the text, and send. The
5
+ conversation restarts from just before that message with the edited text.
6
+
7
+ Install just this plugin:
8
+
9
+ ```sh
10
+ dsh plugin add @1outres/dsh-plugin-edit-message
11
+ ```
12
+
13
+ Or install it with every plugin in the repository through the
14
+ [`@1outres/dsh-plugins`](../dsh-plugins) bundle. Enable one or the other: both
15
+ insert the same `edit-message` row.
16
+
17
+ ## Editing a message
18
+
19
+ Hover a message you sent and click the pencil next to Copy. The bubble turns
20
+ into an editor with the original text.
21
+
22
+ | Key | Action |
23
+ | ----------------------- | ----------------------- |
24
+ | `Ctrl+Enter` / `⌘Enter` | Send the edited message |
25
+ | `Esc` | Cancel; nothing changes |
26
+ | `Enter` | New line |
27
+
28
+ `Enter` does not send, whatever the composer setting says: sending replaces
29
+ the conversation, so it needs the deliberate shortcut or the Send button.
30
+ Sending unchanged text is allowed and regenerates the answer from that point.
31
+
32
+ The button is shown on the prompt that opens each turn. It is not shown on
33
+ steering messages sent while a turn was running, nor in subagent sessions. It
34
+ is dimmed, with the reason in its tooltip, while the session is running, when
35
+ the message has attached files, or when this browser may not fork the session.
36
+
37
+ ## What sending does
38
+
39
+ DSH session logs are append-only, so an edit cannot shorten the current
40
+ session. Instead, Send:
41
+
42
+ 1. Forks the session just before the edited message: through the end of the
43
+ previous turn and the events recorded after it (changed-file summaries,
44
+ titles, model choices, commands). Editing the first message keeps only the
45
+ session's setup.
46
+ 2. Removes any queued prompt the fork inherited. A prompt queued while the
47
+ previous turn was running is logged before that turn ends, and would
48
+ otherwise run ahead of the edited one.
49
+ 3. Keeps the original title on the fork when a later message is edited. When
50
+ the first message is edited, the title is generated again for the new
51
+ topic.
52
+ 4. Sends the edited text, with the original message's images, in the fork.
53
+ 5. Only after the fork accepts the prompt: places the fork where the original
54
+ sits in its workspace, opens it, and archives the original.
55
+
56
+ If any step fails, the original session is not touched, an unused fork is
57
+ archived, and the editor stays open with the error and your text.
58
+
59
+ The archived original stays available under the sidebar's archived filter and
60
+ can be unarchived. The new session has a new id and URL.
61
+
62
+ ## Limits
63
+
64
+ - **Files are not reverted.** Only the conversation goes back; files the agent
65
+ changed after that point stay changed.
66
+ - Messages with attached files cannot be edited: the browser cannot read a
67
+ file attachment back to send it again. Images are sent again; v1 edits the
68
+ text only and cannot add or remove images.
69
+ - Each edit leaves the previous version in the archive. DSH offers no way to
70
+ delete a session.
71
+ - Renaming the fork pins its title, as a manual rename does, so it is no
72
+ longer generated automatically.
73
+ - The Edit button is added to the shipped message bubble, which offers no
74
+ slot for message actions. The plugin wraps the shipped `user` renderer of
75
+ `conversation.chat.node` and appends its button to that bubble's action
76
+ row. If a Harness update changes that row, the button is not shown; if the
77
+ wrapper throws, the slot runtime retires it and the shipped bubble returns.
78
+ - English only.
79
+
80
+ ## How it works
81
+
82
+ The browser half registers a `user` renderer in `conversation.chat.node` at
83
+ priority `-100`, above the shipped one at `0`, and renders the shipped
84
+ renderer inside it unchanged. Its Edit button and editor use copied host
85
+ styles that share only theme tokens. The edit runs through the Session and
86
+ Workspace services the Web UI already uses (`ctx.sessions`,
87
+ `ctx.uiWorkspace`, `ctx.workspaces`). The host half has no behavior and no
88
+ settings.
@@ -0,0 +1,6 @@
1
+ # Installs this plugin on its own (dsh plugin add @1outres/dsh-plugin-edit-message).
2
+ # The @1outres/dsh-plugins bundle inserts the same "edit-message" row, so select
3
+ # one bundle or the other, never both.
4
+ - insert:
5
+ - id: edit-message
6
+ name: "@1outres/dsh-plugin-edit-message"
package/icon.svg ADDED
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none" role="img" aria-labelledby="title">
2
+ <title id="title">Edit Message</title>
3
+ <rect x="2" y="2" width="60" height="60" rx="14" fill="#4D6BFE"/>
4
+ <path d="M15 18.5C15 16.567 16.567 15 18.5 15H40.5C42.433 15 44 16.567 44 18.5V32.5C44 34.433 42.433 36 40.5 36H27L20 42V36H18.5C16.567 36 15 34.433 15 32.5V18.5Z" fill="#FFFFFF" fill-opacity="0.92"/>
5
+ <path d="M21 22H37M21 28H31" stroke="#4D6BFE" stroke-width="2.6" stroke-linecap="round"/>
6
+ <path d="M46.6 30.4L50.6 34.4L36.8 48.2L31.6 49.4L32.8 44.2L46.6 30.4Z" fill="#FFFFFF" stroke="#2E47C9" stroke-width="2" stroke-linejoin="round"/>
7
+ <path d="M43.8 33.2L47.8 37.2" stroke="#2E47C9" stroke-width="2"/>
8
+ </svg>
package/lib/client.js ADDED
@@ -0,0 +1,623 @@
1
+ /* Generated by scripts/build.mjs — do not edit. */
2
+ window.__ModuleLoader__.load({
3
+ id: "@1outres/dsh-plugin-edit-message",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ "use strict";
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
26
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
27
+
28
+ // ../dsh-plugin-edit-message/src/client/index.tsx
29
+ var index_exports = {};
30
+ __export(index_exports, {
31
+ NAMESPACE: () => NAMESPACE,
32
+ NODE_KEY: () => NODE_KEY,
33
+ NODE_PRIORITY: () => NODE_PRIORITY,
34
+ NODE_SLOT: () => NODE_SLOT,
35
+ apply: () => apply,
36
+ inject: () => inject
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // ../dsh-plugin-edit-message/src/client/EditableUserMessage.tsx
41
+ var import_react = require("react");
42
+ var import_react_dom = require("react-dom");
43
+
44
+ // ../dsh-plugin-edit-message/src/client/content.ts
45
+ function isImageRef(value) {
46
+ if (typeof value !== "object" || value === null) return false;
47
+ const ref = value;
48
+ return typeof ref.attachmentId === "string" && typeof ref.mediaType === "string";
49
+ }
50
+ function messageParts(content) {
51
+ const texts = [];
52
+ const images = [];
53
+ let hasFiles = false;
54
+ for (const block of content) {
55
+ if (block.type === "text" && typeof block.text === "string") texts.push(block.text);
56
+ else if (block.type === "image" && isImageRef(block.attachment)) images.push(block.attachment);
57
+ else if (block.type === "file") hasFiles = true;
58
+ }
59
+ return { text: texts.join(""), images, hasFiles };
60
+ }
61
+ function bytesToBase64(bytes) {
62
+ let binary = "";
63
+ const chunk = 32768;
64
+ for (let index = 0; index < bytes.length; index += chunk) {
65
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
66
+ }
67
+ return btoa(binary);
68
+ }
69
+ function promptContent(text, images) {
70
+ return [
71
+ { type: "text", text },
72
+ ...images.map(
73
+ (image) => ({
74
+ type: "image",
75
+ mediaType: image.mediaType,
76
+ data: image.data,
77
+ ...image.name === void 0 ? {} : { name: image.name }
78
+ })
79
+ )
80
+ ];
81
+ }
82
+
83
+ // ../dsh-plugin-edit-message/src/client/icons.tsx
84
+ var import_jsx_runtime = require("react/jsx-runtime");
85
+ function IconEdit({ size = 16 }) {
86
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
87
+ "svg",
88
+ {
89
+ width: size,
90
+ height: size,
91
+ viewBox: "0 0 16 16",
92
+ fill: "none",
93
+ "aria-hidden": "true",
94
+ strokeWidth: 1,
95
+ children: [
96
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
97
+ "path",
98
+ {
99
+ d: "M8.85596 2.69971H4.19971C3.37141 2.69971 2.69992 3.37146 2.69971 4.19971V11.8003C2.69992 12.6285 3.37141 13.3003 4.19971 13.3003H11.8003C12.6283 13.2999 13.3001 12.6283 13.3003 11.8003V7.89893H14.3003V11.8003C14.3001 13.1806 13.1806 14.2999 11.8003 14.3003H4.19971C2.81913 14.3003 1.69992 13.1808 1.69971 11.8003V4.19971C1.69992 2.81918 2.81913 1.69971 4.19971 1.69971H8.85596V2.69971Z",
100
+ fill: "currentColor"
101
+ }
102
+ ),
103
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M7.7849 8.23878L13.888 2.13574", stroke: "currentColor" })
104
+ ]
105
+ }
106
+ );
107
+ }
108
+
109
+ // ../dsh-plugin-edit-message/src/client/EditableUserMessage.tsx
110
+ var import_jsx_runtime2 = require("react/jsx-runtime");
111
+ function editableMessage(node) {
112
+ if (node.kind !== "user") return void 0;
113
+ const location = node.location;
114
+ if (location?.kind !== "step" || location.step?.step !== 1) return void 0;
115
+ const turn = location.turn?.turn;
116
+ const turnStartSeq = location.turn?.start?.seq;
117
+ if (typeof turn !== "number" || turn < 1 || typeof turnStartSeq !== "number") return void 0;
118
+ const content = node.data?.content;
119
+ if (!Array.isArray(content)) return void 0;
120
+ return { turn, turnStartSeq, content, parts: messageParts(content) };
121
+ }
122
+ function wrappedRenderer(entries, self, priority) {
123
+ for (const entry of entries) {
124
+ if (entry.options.key !== "user" || entry.component === self) continue;
125
+ if ((entry.options.priority ?? 0) <= priority) continue;
126
+ return entry.component;
127
+ }
128
+ return void 0;
129
+ }
130
+ function findActionRow(root) {
131
+ const rows = root.querySelectorAll("[data-clock]");
132
+ return rows.length > 0 ? rows[rows.length - 1] ?? null : null;
133
+ }
134
+ function EditButton({
135
+ t,
136
+ unavailable,
137
+ onEdit
138
+ }) {
139
+ const [tip, setTip] = (0, import_react.useState)(false);
140
+ const reasonId = (0, import_react.useId)();
141
+ const label = t("edit");
142
+ const reason = unavailable === void 0 ? void 0 : t(`edit.${unavailable}`);
143
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
144
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
145
+ "button",
146
+ {
147
+ type: "button",
148
+ className: "dsh-em-action",
149
+ "aria-label": label,
150
+ "aria-disabled": unavailable === void 0 ? void 0 : true,
151
+ "aria-describedby": reason === void 0 ? void 0 : reasonId,
152
+ "data-unavailable": unavailable === void 0 ? void 0 : "",
153
+ onClick: unavailable === void 0 ? onEdit : void 0,
154
+ onMouseEnter: () => setTip(true),
155
+ onMouseLeave: () => setTip(false),
156
+ onFocus: () => setTip(true),
157
+ onBlur: () => setTip(false),
158
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IconEdit, {})
159
+ }
160
+ ),
161
+ reason === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { id: reasonId, className: "dsh-em-hidden", children: reason }),
162
+ tip ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-em-tip", role: "tooltip", children: reason ?? label }) : null
163
+ ] });
164
+ }
165
+ function failureText(t, reason, detail) {
166
+ return t(`error.${reason}`, { detail: detail ?? "" });
167
+ }
168
+ function MessageEditor({
169
+ t,
170
+ message,
171
+ onCancel,
172
+ onSend
173
+ }) {
174
+ const [text, setText] = (0, import_react.useState)(message.parts.text);
175
+ const [busy, setBusy] = (0, import_react.useState)(false);
176
+ const [error, setError] = (0, import_react.useState)(null);
177
+ const input = (0, import_react.useRef)(null);
178
+ const mounted = (0, import_react.useRef)(true);
179
+ (0, import_react.useEffect)(
180
+ () => () => {
181
+ mounted.current = false;
182
+ },
183
+ []
184
+ );
185
+ const resize = (0, import_react.useCallback)(() => {
186
+ const element = input.current;
187
+ if (element === null) return;
188
+ element.style.height = "auto";
189
+ element.style.height = `${element.scrollHeight}px`;
190
+ }, []);
191
+ (0, import_react.useLayoutEffect)(() => {
192
+ const element = input.current;
193
+ if (element === null) return;
194
+ resize();
195
+ element.focus();
196
+ element.setSelectionRange(element.value.length, element.value.length);
197
+ }, [resize]);
198
+ const empty = text.trim().length === 0;
199
+ const send = async () => {
200
+ if (busy || empty) return;
201
+ setBusy(true);
202
+ setError(null);
203
+ const outcome = await onSend(text);
204
+ if (!mounted.current) return;
205
+ setBusy(false);
206
+ if (!outcome.ok) setError(failureText(t, outcome.reason, outcome.detail));
207
+ };
208
+ const onKeyDown = (event) => {
209
+ if (event.nativeEvent.isComposing) return;
210
+ if (event.key === "Escape") {
211
+ event.preventDefault();
212
+ event.stopPropagation();
213
+ if (!busy) onCancel();
214
+ } else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
215
+ event.preventDefault();
216
+ event.stopPropagation();
217
+ void send();
218
+ }
219
+ };
220
+ const images = message.parts.images.length;
221
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-em-editor", "data-plugin": "@1outres/dsh-plugin-edit-message", children: [
222
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-em-box", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
223
+ "textarea",
224
+ {
225
+ ref: input,
226
+ className: "dsh-em-input",
227
+ "aria-label": t("editor.aria"),
228
+ value: text,
229
+ rows: 1,
230
+ disabled: busy,
231
+ onChange: (event) => {
232
+ setText(event.target.value);
233
+ resize();
234
+ },
235
+ onKeyDown
236
+ }
237
+ ) }),
238
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-em-footer", children: [
239
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-em-notes", children: [
240
+ error === null ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-em-error", role: "alert", children: error }),
241
+ images > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("editor.images", { count: images }) }) : null
242
+ ] }),
243
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-em-buttons", children: [
244
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
245
+ "button",
246
+ {
247
+ type: "button",
248
+ className: "dsh-em-button dsh-em-cancel",
249
+ disabled: busy,
250
+ onClick: onCancel,
251
+ children: t("editor.cancel")
252
+ }
253
+ ),
254
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
255
+ "button",
256
+ {
257
+ type: "button",
258
+ className: "dsh-em-button dsh-em-send",
259
+ disabled: busy || empty,
260
+ onClick: () => void send(),
261
+ children: busy ? t("editor.sending") : t("editor.send")
262
+ }
263
+ )
264
+ ] })
265
+ ] })
266
+ ] });
267
+ }
268
+ function createEditableUserMessage(deps) {
269
+ function EditableUserMessage(props) {
270
+ const Wrapped = wrappedRenderer(deps.entries(), EditableUserMessage, deps.priority);
271
+ if (Wrapped === void 0) throw new Error("edit-message: no user message renderer to wrap");
272
+ const { node, sessionId, useSession, useAccess } = props;
273
+ const running = useSession((snapshot) => snapshot.running);
274
+ const subagent = useSession(
275
+ (snapshot) => snapshot.subagent !== null && snapshot.subagent !== void 0
276
+ );
277
+ const forkAllowed = useAccess("session/fork");
278
+ const [editing, setEditing] = (0, import_react.useState)(false);
279
+ const [seat, setSeat] = (0, import_react.useState)(null);
280
+ const wrap = (0, import_react.useRef)(null);
281
+ const message = subagent ? void 0 : editableMessage(node);
282
+ const visible = message !== void 0;
283
+ (0, import_react.useLayoutEffect)(() => {
284
+ const root = wrap.current;
285
+ if (editing || !visible || root === null) return;
286
+ const span = document.createElement("span");
287
+ span.className = "dsh-em-seat";
288
+ const attach = () => {
289
+ const row = findActionRow(root);
290
+ if (row === null) span.remove();
291
+ else if (span.parentElement !== row) row.append(span);
292
+ };
293
+ attach();
294
+ setSeat(span);
295
+ const observer = typeof MutationObserver === "undefined" ? void 0 : new MutationObserver(attach);
296
+ observer?.observe(root, { childList: true, subtree: true });
297
+ return () => {
298
+ observer?.disconnect();
299
+ span.remove();
300
+ setSeat(null);
301
+ };
302
+ }, [editing, visible]);
303
+ if (editing && message !== void 0) {
304
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
305
+ MessageEditor,
306
+ {
307
+ t: deps.t,
308
+ message,
309
+ onCancel: () => setEditing(false),
310
+ onSend: async (text) => {
311
+ const outcome = await deps.edit({
312
+ sessionId,
313
+ turn: message.turn,
314
+ turnStartSeq: message.turnStartSeq,
315
+ content: message.content,
316
+ text
317
+ });
318
+ if (outcome.ok) setEditing(false);
319
+ return outcome;
320
+ }
321
+ }
322
+ );
323
+ }
324
+ const unavailable = !forkAllowed ? "noAccess" : running ? "running" : message?.parts.hasFiles ? "files" : void 0;
325
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ref: wrap, style: { display: "contents" }, children: [
326
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Wrapped, { ...props }),
327
+ seat !== null && message !== void 0 ? (0, import_react_dom.createPortal)(
328
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EditButton, { t: deps.t, unavailable, onEdit: () => setEditing(true) }),
329
+ seat
330
+ ) : null
331
+ ] });
332
+ }
333
+ return EditableUserMessage;
334
+ }
335
+
336
+ // ../dsh-plugin-edit-message/src/client/boundary.ts
337
+ function durableEvents(entries) {
338
+ const events = [];
339
+ for (const entry of entries) if (entry.type === "event") events.push(entry.event);
340
+ return events;
341
+ }
342
+ function opensExchange(event) {
343
+ if (event.type === "turn/start" || event.type === "agent/inbox/spliced") return true;
344
+ return event.type === "user/message" && event.surfaceOp === "append";
345
+ }
346
+ function forkBoundary(events, turn, turnStartSeq) {
347
+ const first = events[0];
348
+ if (first === void 0 || first.seq > turnStartSeq) {
349
+ return { kind: "load", through: Math.max(0, turnStartSeq - 1) };
350
+ }
351
+ let anchor;
352
+ for (const event of events) {
353
+ if (event.seq >= turnStartSeq) break;
354
+ if (event.type === "turn/end") anchor = event.seq;
355
+ }
356
+ if (anchor === void 0) {
357
+ if (first.seq > 0) {
358
+ const through = first.seq > turnStartSeq - 1 ? turnStartSeq - 1 : 0;
359
+ return { kind: "load", through: Math.max(0, through) };
360
+ }
361
+ if (turn > 1) return { kind: "invalid" };
362
+ anchor = -1;
363
+ }
364
+ let cut = anchor;
365
+ for (const event of events) {
366
+ if (event.seq <= anchor) continue;
367
+ if (event.seq >= turnStartSeq || opensExchange(event)) break;
368
+ cut = event.seq;
369
+ }
370
+ return cut < 0 ? { kind: "invalid" } : { kind: "cut", seq: cut };
371
+ }
372
+
373
+ // ../dsh-plugin-edit-message/src/client/edit.ts
374
+ var REFERENCE_SOURCE = "controllerOperation";
375
+ var MAX_HISTORY_LOADS = 3;
376
+ var EditError = class extends Error {
377
+ constructor(reason, detail) {
378
+ super(detail ?? reason);
379
+ __publicField(this, "reason", reason);
380
+ __publicField(this, "detail", detail);
381
+ }
382
+ };
383
+ function describe(error) {
384
+ if (error instanceof Error) {
385
+ const rpc = error.rpcError;
386
+ if (typeof rpc?.message === "string" && rpc.message.length > 0) return rpc.message;
387
+ return error.message;
388
+ }
389
+ return String(error);
390
+ }
391
+ function fail(reason, detail) {
392
+ return detail === void 0 ? { ok: false, reason } : { ok: false, reason, detail };
393
+ }
394
+ async function resolveCut(source, request) {
395
+ for (let attempt = 0; attempt <= MAX_HISTORY_LOADS; attempt++) {
396
+ const events = durableEvents(source.eventSource.getSnapshot().entries);
397
+ const boundary = forkBoundary(events, request.turn, request.turnStartSeq);
398
+ if (boundary.kind === "cut") return boundary.seq;
399
+ if (boundary.kind === "invalid" || attempt === MAX_HISTORY_LOADS) break;
400
+ const before = events[0]?.seq;
401
+ await source.session.loadThrough(boundary.through);
402
+ const after = durableEvents(source.eventSource.getSnapshot().entries)[0]?.seq;
403
+ if (after === before) break;
404
+ }
405
+ throw new EditError("boundary");
406
+ }
407
+ async function loadImages(source, request) {
408
+ const images = [];
409
+ for (const ref of messageParts(request.content).images) {
410
+ const read = await source.session.readAttachment(ref.attachmentId);
411
+ if (!read.ok) throw new EditError("attachment", read.error.message);
412
+ const name = read.value.attachment.name ?? ref.name;
413
+ images.push({
414
+ mediaType: read.value.attachment.mediaType,
415
+ data: bytesToBase64(read.value.data),
416
+ ...name === void 0 ? {} : { name }
417
+ });
418
+ }
419
+ return images;
420
+ }
421
+ function pendingIds(inbox) {
422
+ if (typeof inbox !== "object" || inbox === null) return [];
423
+ const ids = [];
424
+ for (const target of ["next-turn", "next-step"]) {
425
+ const items = inbox[target];
426
+ if (!Array.isArray(items)) continue;
427
+ for (const item of items) {
428
+ const id = item?.id;
429
+ if (typeof id === "string" && id.length > 0) ids.push(id);
430
+ }
431
+ }
432
+ return ids;
433
+ }
434
+ async function clearInheritedQueue(sessions, child) {
435
+ let inbox = child.session.projections.faceOf("inbox").getSnapshot();
436
+ if (inbox === void 0) {
437
+ await sessions.refreshProjections(child.sessionId);
438
+ inbox = child.session.projections.faceOf("inbox").getSnapshot() ?? sessions.list.getSnapshot().projectionsBySession?.[child.sessionId]?.values?.inbox;
439
+ }
440
+ for (const id of pendingIds(inbox)) {
441
+ const removed = await child.session.updateQueue(id, { kind: "remove" });
442
+ if (!removed.ok) throw new EditError("prepare", removed.error.message);
443
+ }
444
+ }
445
+ async function placeBeside(deps, sourceId, childId) {
446
+ const workspaces = deps.workspaces;
447
+ if (workspaces === void 0) return;
448
+ const owner = workspaces.list.getSnapshot().items.find((item) => item.sessionIds.includes(sourceId));
449
+ if (owner === void 0) return;
450
+ try {
451
+ await workspaces.insertSessionBefore(owner.workspaceId, childId, sourceId);
452
+ } catch (error) {
453
+ deps.warn?.("edit-message: could not place the new session beside the original", error);
454
+ }
455
+ }
456
+ async function runEdit(deps, request) {
457
+ const source = deps.sessions.binding(request.sessionId);
458
+ if (source === void 0) return fail("unavailable");
459
+ if (source.session.getSnapshot().running) return fail("running");
460
+ if (request.text.trim().length === 0) return fail("empty");
461
+ if (messageParts(request.content).hasFiles) return fail("files");
462
+ let cut;
463
+ try {
464
+ cut = await resolveCut(source, request);
465
+ } catch {
466
+ return fail("boundary");
467
+ }
468
+ let images;
469
+ try {
470
+ images = await loadImages(source, request);
471
+ } catch (error) {
472
+ return fail("attachment", error instanceof EditError ? error.detail : describe(error));
473
+ }
474
+ let childId;
475
+ try {
476
+ childId = await deps.sessions.fork({
477
+ sessionId: request.sessionId,
478
+ atSeq: cut,
479
+ increaseTitle: false
480
+ });
481
+ } catch (error) {
482
+ return fail("fork", describe(error));
483
+ }
484
+ const reference = deps.sessions.retain(childId, { source: REFERENCE_SOURCE });
485
+ try {
486
+ const child = await reference.ready;
487
+ await clearInheritedQueue(deps.sessions, child);
488
+ const title = deps.sessions.list.getSnapshot().byId[request.sessionId]?.title;
489
+ if (request.turn > 1 && title !== void 0 && title.length > 0) {
490
+ const renamed = await child.session.rename(title);
491
+ if (!renamed.ok) deps.warn?.("edit-message: could not keep the title", renamed.error);
492
+ }
493
+ const sent = await child.session.prompt(promptContent(request.text, images), "queue");
494
+ if (!sent.ok) throw new EditError("prompt", sent.error.message);
495
+ } catch (error) {
496
+ reference.release();
497
+ try {
498
+ await deps.uiWorkspace.archiveSession(childId);
499
+ } catch (cleanup) {
500
+ deps.warn?.("edit-message: could not archive the unused fork", cleanup);
501
+ }
502
+ if (error instanceof EditError) return fail(error.reason, error.detail);
503
+ return fail("prepare", describe(error));
504
+ }
505
+ await placeBeside(deps, request.sessionId, childId);
506
+ deps.uiWorkspace.openSession(childId);
507
+ reference.release();
508
+ try {
509
+ await deps.uiWorkspace.archiveSession(request.sessionId);
510
+ } catch (error) {
511
+ deps.warn?.("edit-message: could not archive the original session", error);
512
+ }
513
+ return { ok: true, sessionId: childId };
514
+ }
515
+
516
+ // ../dsh-plugin-edit-message/src/client/i18n.ts
517
+ var en = {
518
+ edit: "Edit",
519
+ "edit.running": "Stop the running turn to edit this message",
520
+ "edit.files": "Messages with attached files can't be edited",
521
+ "edit.noAccess": "You can't branch this session",
522
+ "editor.aria": "Edit message",
523
+ "editor.cancel": "Cancel",
524
+ "editor.send": "Send",
525
+ "editor.sending": "Sending\u2026",
526
+ "editor.images": "{count} image(s) from the original message are sent again.",
527
+ "error.unavailable": "This session is not loaded.",
528
+ "error.running": "The session is running. Stop it, then send again.",
529
+ "error.empty": "Enter a message.",
530
+ "error.files": "Messages with attached files can't be edited.",
531
+ "error.boundary": "Could not find where this message starts in the session history.",
532
+ "error.attachment": "Could not read the original images: {detail}",
533
+ "error.fork": "Could not create the new session: {detail}",
534
+ "error.prepare": "Could not prepare the new session: {detail}",
535
+ "error.prompt": "The new session did not accept the message: {detail}"
536
+ };
537
+
538
+ // ../dsh-plugin-edit-message/src/client/styles.ts
539
+ var EDIT_CSS = [
540
+ ".dsh-em-seat{display:inline-flex;position:relative}",
541
+ ".dsh-em-action{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:6px;display:inline-flex}",
542
+ ".dsh-em-action svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px))}",
543
+ ".dsh-em-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
544
+ ".dsh-em-action:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}",
545
+ ".dsh-em-action[data-unavailable]{cursor:default;opacity:.4}",
546
+ ".dsh-em-action[data-unavailable]:hover{color:var(--dsw-alias-label-tertiary);background:0 0}",
547
+ ".dsh-em-tip{position:absolute;top:calc(100% + 4px);left:50%;transform:translate(-50%);z-index:100;width:max-content;max-width:280px;box-sizing:border-box;padding:3px 7px;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-tooltip-bg);color:var(--dsw-alias-toast-label);font-size:13px;line-height:20px;white-space:pre-line;overflow-wrap:break-word;pointer-events:none;animation:dsh-em-tip-in .15s ease-in-out}",
548
+ "@keyframes dsh-em-tip-in{0%{opacity:0}to{opacity:1}}",
549
+ ".dsh-em-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}",
550
+ ".dsh-em-editor{box-sizing:border-box;width:100%;display:flex;flex-direction:column;align-items:stretch;gap:8px}",
551
+ ".dsh-em-box{box-sizing:border-box;background:var(--dsw-specific-bubble);border-radius:var(--dsw-radius-xl);padding:10px 16px;border:.5px solid transparent}",
552
+ ".dsh-em-box:focus-within{border-color:var(--dsw-alias-border-l3)}",
553
+ ".dsh-em-input{display:block;box-sizing:border-box;width:100%;min-height:calc(22px + var(--dsh-content-font-delta,0px));max-height:50vh;margin:0;padding:0;border:none;outline:none;resize:none;overflow-y:auto;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));white-space:pre-wrap;word-break:break-word}",
554
+ ".dsh-em-input:disabled{color:var(--dsw-alias-label-secondary)}",
555
+ ".dsh-em-footer{display:flex;align-items:flex-start;gap:8px}",
556
+ ".dsh-em-notes{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px))}",
557
+ ".dsh-em-error{color:var(--dsw-alias-state-error-primary)}",
558
+ ".dsh-em-buttons{flex:none;display:flex;align-items:center;gap:8px}",
559
+ ".dsh-em-button{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 10px;border:none;border-radius:var(--dsw-radius-sm);cursor:pointer;font:inherit;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);background:transparent}",
560
+ ".dsh-em-button:disabled{cursor:not-allowed;opacity:.4}",
561
+ ".dsh-em-button:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}",
562
+ ".dsh-em-cancel{border:.5px solid var(--dsw-alias-border-l3)}",
563
+ ".dsh-em-cancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
564
+ ".dsh-em-send{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}",
565
+ ".dsh-em-send:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}",
566
+ "@media (prefers-reduced-motion:reduce){.dsh-em-tip{animation:none}}"
567
+ ].join("\n");
568
+
569
+ // ../dsh-plugin-edit-message/src/client/index.tsx
570
+ var inject = ["slots", "locale", "sessions", "uiWorkspace", "workspaces"];
571
+ var PLUGIN_ID = "@1outres/dsh-plugin-edit-message";
572
+ var NAMESPACE = "edit-message";
573
+ var NODE_SLOT = "conversation.chat.node";
574
+ var NODE_KEY = "user";
575
+ var NODE_PRIORITY = -100;
576
+ var CHAT_NAMESPACE = "chat";
577
+ function apply(ctx) {
578
+ ctx.effect(() => ctx.locale.register(NAMESPACE, { en }), "edit-message: dictionaries");
579
+ ctx.effect(() => {
580
+ const tag = document.createElement("style");
581
+ tag.dataset.plugin = PLUGIN_ID;
582
+ tag.dataset.pluginCss = `${PLUGIN_ID}/edit.css`;
583
+ tag.textContent = EDIT_CSS;
584
+ document.head.append(tag);
585
+ return () => {
586
+ tag.remove();
587
+ };
588
+ }, "edit-message: stylesheet");
589
+ const slots = ctx.slots;
590
+ if (typeof slots.entries !== "function") {
591
+ console.warn("edit-message: this Harness exposes no slot entries; the plugin stays inactive");
592
+ return;
593
+ }
594
+ const entries = () => slots.entries?.(NODE_SLOT) ?? [];
595
+ const View = createEditableUserMessage({
596
+ entries,
597
+ priority: NODE_PRIORITY,
598
+ t: ctx.locale.bind(NAMESPACE),
599
+ edit: (request) => runEdit(
600
+ {
601
+ sessions: ctx.sessions,
602
+ uiWorkspace: ctx.uiWorkspace,
603
+ workspaces: ctx.workspaces,
604
+ warn: (message, error) => console.warn(message, error)
605
+ },
606
+ request
607
+ )
608
+ });
609
+ ctx.effect(
610
+ () => slots.inject(
611
+ NODE_SLOT,
612
+ () => slots.register(
613
+ { name: NODE_SLOT, key: NODE_KEY, priority: NODE_PRIORITY, locale: CHAT_NAMESPACE },
614
+ View
615
+ )
616
+ ),
617
+ "edit-message: user message renderer"
618
+ );
619
+ }
620
+
621
+ return module.exports;
622
+ }
623
+ });
package/lib/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // ../dsh-plugin-edit-message/src/index.ts
2
+ function apply(_ctx) {
3
+ }
4
+ export {
5
+ apply
6
+ };
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Edit Message",
4
+ "description": "Adds an Edit button beside Copy on your sent messages. Sending the edited text restarts the conversation from just before that message and archives the old one. Files are not reverted."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "编辑消息",
4
+ "description": "在已发送消息的复制按钮旁添加编辑按钮。发送修改后的内容会从该消息之前重新开始对话,并归档原会话。文件改动不会回退。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@1outres/dsh-plugin-edit-message",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.6.0",
4
+ "description": "Edit a sent message in the DeepSeek Harness Web UI: the conversation restarts from just before it with the edited text.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/1outres/my-dsh-plugins.git",
8
+ "directory": "packages/dsh-plugin-edit-message"
9
+ },
10
+ "type": "module",
11
+ "main": "lib/index.js",
12
+ "exports": {
13
+ ".": "./lib/index.js",
14
+ "./client": "./lib/client.js",
15
+ "./locale/*.json": "./locale/*.json",
16
+ "./icon": "./icon.svg",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "lib",
21
+ "cordis.patch.yml",
22
+ "locale/*.json",
23
+ "icon.svg"
24
+ ],
25
+ "keywords": [
26
+ "dsh",
27
+ "dsh-plugin",
28
+ "edit-message",
29
+ "rewind",
30
+ "deepseek-harness"
31
+ ],
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "engines": {
37
+ "dsh": ">=0.2.1-rc.5 <0.3.0",
38
+ "node": ">=22"
39
+ },
40
+ "peerDependencies": {
41
+ "@deepseek-ai/cordis": "~4.0.5",
42
+ "react": "^18.2.0"
43
+ },
44
+ "dsh": {
45
+ "bundle": {
46
+ "patch": [
47
+ "./cordis.patch.yml"
48
+ ]
49
+ },
50
+ "client": {
51
+ "platform": "web",
52
+ "inject": [
53
+ "@deepseek-ai/dsh-client-ui-renderer",
54
+ "@deepseek-ai/dsh-api-session-controller",
55
+ "@deepseek-ai/dsh-api-workspace-controller",
56
+ "@deepseek-ai/dsh-client-ui-workspace",
57
+ "@deepseek-ai/dsh-client-ui-chat"
58
+ ]
59
+ }
60
+ }
6
61
  }