dsh-prompt-rewriter 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/README.zh.md +8 -0
- package/client.js +242 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,14 @@ The third column is not something you wrote. It read your sentence and filled it
|
|
|
36
36
|
|
|
37
37
|
Changed your mind mid-flight? Click again to cancel. The draft stays exactly as it was — no half-finished text left behind.
|
|
38
38
|
|
|
39
|
+
### You can navigate away mid-rewrite
|
|
40
|
+
|
|
41
|
+
Once you've clicked, you don't have to sit and wait. **Switch to another session and the task keeps running.**
|
|
42
|
+
|
|
43
|
+
Come back and the result is already in the composer; if it hasn't finished, the button is still the spinning tail. Two sessions can each run their own rewrite without interfering.
|
|
44
|
+
|
|
45
|
+
That wasn't true in 1.0.x: switching sessions aborted the task, and coming back showed nothing.
|
|
46
|
+
|
|
39
47
|
### The four button states
|
|
40
48
|
|
|
41
49
|

|
package/README.zh.md
CHANGED
|
@@ -37,6 +37,14 @@
|
|
|
37
37
|
|
|
38
38
|
中途想放弃就再点一下,草稿保持原样,不会给你留下半截文字。
|
|
39
39
|
|
|
40
|
+
### 增强过程中可以切走
|
|
41
|
+
|
|
42
|
+
点了按钮之后不必守着它。**切到别的会话,任务会继续跑完。**
|
|
43
|
+
|
|
44
|
+
回来的时候,结果已经写进输入框了;如果还没跑完,按钮仍然是那个旋转的鱼尾。两个会话可以同时各跑一个,互不影响。
|
|
45
|
+
|
|
46
|
+
这一点在 1.0.x 里不是这样:那时切换会话会直接中断任务,回来什么也没有。
|
|
47
|
+
|
|
40
48
|
### 按钮的四种状态
|
|
41
49
|
|
|
42
50
|

|
package/client.js
CHANGED
|
@@ -248,6 +248,103 @@ window.__ModuleLoader__.load({
|
|
|
248
248
|
);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* One enhancement task per session, owned OUTSIDE the component tree.
|
|
253
|
+
*
|
|
254
|
+
* The composer control is mounted per session and unmounted the moment the
|
|
255
|
+
* user switches away. Keeping the in-flight request (and its abort
|
|
256
|
+
* controller) inside the component therefore tied the task's lifetime to
|
|
257
|
+
* whichever session happened to be on screen: unmounting aborted the fetch
|
|
258
|
+
* and the cleanup flagged the component dead, so the reply was discarded
|
|
259
|
+
* even if it arrived. Switching sessions looked like "the enhancement was
|
|
260
|
+
* cancelled", when in fact the UI had simply outlived it.
|
|
261
|
+
*
|
|
262
|
+
* This registry is the decoupling point. A task belongs to a sessionId, not
|
|
263
|
+
* to a component instance, so it survives unmount, keeps running, and is
|
|
264
|
+
* delivered to whichever composer mounts that session later. Two sessions
|
|
265
|
+
* get independent entries and cannot interfere.
|
|
266
|
+
*
|
|
267
|
+
* @returns a small imperative store with subscribe/getSnapshot per session.
|
|
268
|
+
*/
|
|
269
|
+
function createTaskStore() {
|
|
270
|
+
/** sessionId -> task record. Entries are removed once settled and consumed. */
|
|
271
|
+
const tasks = new Map();
|
|
272
|
+
/** sessionId -> listener set, so a remount can re-render on progress. */
|
|
273
|
+
const listeners = new Map();
|
|
274
|
+
/** Bumped whenever any task changes, so snapshots are stable to compare. */
|
|
275
|
+
let version = 0;
|
|
276
|
+
/** Cache of published snapshots, keyed by sessionId, invalidated by version. */
|
|
277
|
+
const snapshots = new Map();
|
|
278
|
+
|
|
279
|
+
const notify = (sessionId) => {
|
|
280
|
+
version += 1;
|
|
281
|
+
snapshots.clear();
|
|
282
|
+
const set = listeners.get(sessionId);
|
|
283
|
+
if (set === undefined) return;
|
|
284
|
+
for (const listener of set) listener();
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The public view of a session's task. `null` means "nothing known", which
|
|
289
|
+
* is what the control renders when the session has no work.
|
|
290
|
+
*/
|
|
291
|
+
const snapshot = (sessionId) => {
|
|
292
|
+
const cached = snapshots.get(sessionId);
|
|
293
|
+
if (cached !== undefined) return cached;
|
|
294
|
+
const task = tasks.get(sessionId);
|
|
295
|
+
const next = task === undefined
|
|
296
|
+
? null
|
|
297
|
+
: Object.freeze({ phase: task.phase, failure: task.failure, announce: task.announce, contextAware: task.contextAware, restore: task.restore, result: task.result });
|
|
298
|
+
snapshots.set(sessionId, next);
|
|
299
|
+
return next;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const subscribe = (sessionId, listener) => {
|
|
303
|
+
let set = listeners.get(sessionId);
|
|
304
|
+
if (set === undefined) {
|
|
305
|
+
set = new Set();
|
|
306
|
+
listeners.set(sessionId, set);
|
|
307
|
+
}
|
|
308
|
+
set.add(listener);
|
|
309
|
+
return () => {
|
|
310
|
+
set.delete(listener);
|
|
311
|
+
if (set.size === 0) listeners.delete(sessionId);
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/** Read-or-create the record for a session. */
|
|
316
|
+
const ensure = (sessionId) => {
|
|
317
|
+
let task = tasks.get(sessionId);
|
|
318
|
+
if (task === undefined) {
|
|
319
|
+
task = { phase: 'idle', failure: null, announce: null, contextAware: false, restore: null, controller: null };
|
|
320
|
+
tasks.set(sessionId, task);
|
|
321
|
+
}
|
|
322
|
+
return task;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/** Apply a patch to a session's record and publish it. */
|
|
326
|
+
const patch = (sessionId, changes) => {
|
|
327
|
+
const task = ensure(sessionId);
|
|
328
|
+
Object.assign(task, changes);
|
|
329
|
+
notify(sessionId);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/** Drop a session's record entirely (task finished and was consumed). */
|
|
333
|
+
const clear = (sessionId) => {
|
|
334
|
+
if (tasks.delete(sessionId)) notify(sessionId);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return { snapshot, subscribe, ensure, patch, clear, get tasks() { return tasks; } };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Sessions whose enhancement finished while their composer was unmounted and
|
|
342
|
+
* nobody has looked at them yet. On remount the control picks these up and
|
|
343
|
+
* adopts the result, so the user sees the completed task on return instead of
|
|
344
|
+
* a silent loss.
|
|
345
|
+
*/
|
|
346
|
+
const taskStore = createTaskStore();
|
|
347
|
+
|
|
251
348
|
/**
|
|
252
349
|
* Composer-side enhancement control: sends the current draft to this
|
|
253
350
|
* package's Host half, then replaces the draft in place — never submits.
|
|
@@ -263,34 +360,33 @@ window.__ModuleLoader__.load({
|
|
|
263
360
|
// The composer's own attachment list. Only these images may be referenced;
|
|
264
361
|
// images from earlier turns live in the session log and are never read here.
|
|
265
362
|
const attachmentIds = useInput((state) => state.attachmentIds);
|
|
266
|
-
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
const
|
|
363
|
+
// Task state lives in the module-level registry, keyed by session, so it
|
|
364
|
+
// outlives this component instance. Switching sessions only changes which
|
|
365
|
+
// entry is read; it never cancels work.
|
|
366
|
+
//
|
|
367
|
+
// A version counter forces a re-render when the task changes while this
|
|
368
|
+
// component is mounted (progress, failure, completion). The counter is
|
|
369
|
+
// local state only — it is not the task, so losing it on unmount costs
|
|
370
|
+
// nothing and cannot cancel work.
|
|
371
|
+
const [, bumpTaskVersion] = useState(0);
|
|
372
|
+
useEffect(() => taskStore.subscribe(sessionId, () => bumpTaskVersion((v) => v + 1)), [sessionId]);
|
|
373
|
+
// The snapshot is read straight from the registry on every render. The version
|
|
374
|
+
// counter above is what triggers those renders; the value itself never lives
|
|
375
|
+
// in component state, so unmounting cannot lose or cancel a task.
|
|
376
|
+
const task = taskStore.snapshot(sessionId);
|
|
377
|
+
const busy = task?.phase === 'running';
|
|
378
|
+
const failure = task?.failure ?? null;
|
|
379
|
+
const announce = task?.announce ?? null;
|
|
380
|
+
const contextAware = task?.contextAware === true;
|
|
381
|
+
const restore = task?.restore ?? null;
|
|
281
382
|
const latestRev = useRef(draftRev);
|
|
282
383
|
latestRev.current = draftRev;
|
|
283
384
|
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
alive.current = false;
|
|
290
|
-
flight.current?.abort();
|
|
291
|
-
flight.current = null;
|
|
292
|
-
};
|
|
293
|
-
}, []);
|
|
385
|
+
// NO abort on unmount. The component unmounts whenever the user switches
|
|
386
|
+
// away from this session, and aborting there is exactly what made the
|
|
387
|
+
// enhancement stop; the request now belongs to the registry and keeps
|
|
388
|
+
// running until it settles on its own.
|
|
389
|
+
useEffect(() => () => {}, []);
|
|
294
390
|
|
|
295
391
|
const replaceDraft = useCallback(
|
|
296
392
|
(text, before, rev) => inputActions?.insertText(text, { start: 0, end: before.length, draftRev: rev }) === true,
|
|
@@ -301,16 +397,15 @@ window.__ModuleLoader__.load({
|
|
|
301
397
|
const undo = useCallback(() => {
|
|
302
398
|
if (restore === null) return;
|
|
303
399
|
if (draft !== restore.produced) {
|
|
304
|
-
|
|
400
|
+
taskStore.patch(sessionId, { restore: null });
|
|
305
401
|
return;
|
|
306
402
|
}
|
|
307
403
|
// Select over the draft the editor actually holds, so any normalization
|
|
308
404
|
// the composer applied to the model output cannot leave a residue.
|
|
309
405
|
if (replaceDraft(restore.text, draft, draftRev)) {
|
|
310
|
-
|
|
311
|
-
setFailure(null);
|
|
406
|
+
taskStore.patch(sessionId, { restore: null, failure: null });
|
|
312
407
|
}
|
|
313
|
-
}, [draft, draftRev, replaceDraft, restore]);
|
|
408
|
+
}, [draft, draftRev, replaceDraft, restore, sessionId]);
|
|
314
409
|
|
|
315
410
|
/** Encode bytes as canonical base64 for the JSON request body. */
|
|
316
411
|
function bytesToBase64(bytes) {
|
|
@@ -365,25 +460,42 @@ window.__ModuleLoader__.load({
|
|
|
365
460
|
return { images, skipped };
|
|
366
461
|
}
|
|
367
462
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Start or pause this session's enhancement.
|
|
465
|
+
*
|
|
466
|
+
* The request runs against the REGISTRY entry, not this closure, so neither
|
|
467
|
+
* unmounting (session switch) nor a re-render can cancel it. `phase` in the
|
|
468
|
+
* record is the single source of truth for busy/idle, which also makes the
|
|
469
|
+
* button state correct when the user returns mid-flight.
|
|
470
|
+
*/
|
|
471
|
+
const run = useCallback(() => {
|
|
472
|
+
const task = taskStore.ensure(sessionId);
|
|
473
|
+
// Second click means "pause": abort the request this task owns. Because the
|
|
474
|
+
// controller is stored on the record, it is still reachable no matter which
|
|
475
|
+
// component instance asked.
|
|
476
|
+
if (task.phase === 'running' && task.controller !== null) {
|
|
477
|
+
task.controller.abort();
|
|
371
478
|
return;
|
|
372
479
|
}
|
|
373
480
|
const before = draft;
|
|
374
481
|
if (before.trim().length === 0) {
|
|
375
|
-
|
|
376
|
-
setRestore(null);
|
|
377
|
-
setAnnounce(null);
|
|
482
|
+
taskStore.patch(sessionId, { failure: null, restore: null, announce: null });
|
|
378
483
|
return;
|
|
379
484
|
}
|
|
380
485
|
const rev = draftRev;
|
|
381
486
|
const controller = new AbortController();
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
487
|
+
taskStore.patch(sessionId, {
|
|
488
|
+
phase: 'running',
|
|
489
|
+
failure: null,
|
|
490
|
+
announce: null,
|
|
491
|
+
controller,
|
|
492
|
+
// The draft this run rewrites; captured so the reply is validated
|
|
493
|
+
// against the text that was actually sent.
|
|
494
|
+
origin: before,
|
|
495
|
+
originRev: rev,
|
|
496
|
+
});
|
|
497
|
+
// Deliberately a floating async task owned by the store: nothing awaits it,
|
|
498
|
+
// and no component cleanup can reach its abort controller.
|
|
387
499
|
(async () => {
|
|
388
500
|
try {
|
|
389
501
|
// Read the composer's images whenever the draft has any. The Host owns
|
|
@@ -407,57 +519,114 @@ window.__ModuleLoader__.load({
|
|
|
407
519
|
});
|
|
408
520
|
if (!response.ok) throw new Error(t('state.transport', { status: response.status }));
|
|
409
521
|
const payload = await response.json();
|
|
410
|
-
if (!alive.current || mine !== ticket.current) return;
|
|
411
522
|
if (payload?.ok !== true) throw new Error(payload?.message || t('state.failed'));
|
|
412
523
|
const text = typeof payload.text === 'string' ? payload.text : '';
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (!replaceDraft(text, before, rev)) {
|
|
418
|
-
setFailure(t('state.refused'));
|
|
524
|
+
if (text.trim().length === 0) {
|
|
525
|
+
taskStore.patch(sessionId, { restore: null });
|
|
419
526
|
return;
|
|
420
527
|
}
|
|
421
|
-
// Record whether this run was context-aware so the seat can show it.
|
|
422
|
-
setContextAware(payload?.mode === 'context');
|
|
423
|
-
// Say so when a picture could not be read, rather than silently
|
|
424
|
-
// rewriting without it.
|
|
425
528
|
const skippedImages = Number(payload?.imagesSkipped) || 0;
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
529
|
+
// `applyResult` is the live delivery path, supplied by whichever composer
|
|
530
|
+
// is currently mounted for this session. When nobody is mounted it is
|
|
531
|
+
// undefined and the parked result waits for the remount.
|
|
532
|
+
const apply = taskStore.ensure(sessionId).applyResult;
|
|
533
|
+
const outcome = typeof apply === 'function' ? apply(text, before, rev) : 'parked';
|
|
534
|
+
taskStore.patch(sessionId, {
|
|
535
|
+
contextAware: payload?.mode === 'context',
|
|
536
|
+
announce: skippedImages > 0 ? t('state.imagesSkipped', { count: skippedImages }) : null,
|
|
537
|
+
});
|
|
538
|
+
if (outcome === 'applied') {
|
|
539
|
+
// The undo offer is only meaningful once the text actually landed in the
|
|
540
|
+
// editor; offering it after a refused or superseded write would claim a
|
|
541
|
+
// swap that never happened.
|
|
542
|
+
taskStore.patch(sessionId, { restore: { text: before, produced: null, pending: true } });
|
|
543
|
+
} else if (outcome === 'refused') {
|
|
544
|
+
taskStore.patch(sessionId, { restore: null, failure: t('state.refused') });
|
|
545
|
+
} else if (outcome === 'parked') {
|
|
546
|
+
// Nobody is mounted for this session: keep the result for the next mount.
|
|
547
|
+
taskStore.patch(sessionId, { result: { text, originRev: rev, origin: before }, restore: null });
|
|
548
|
+
} else {
|
|
549
|
+
// The draft moved on while the user was away; report it instead of
|
|
550
|
+
// overwriting text they have since edited.
|
|
551
|
+
taskStore.patch(sessionId, { restore: null, announce: t('state.stale') });
|
|
552
|
+
}
|
|
431
553
|
} catch (error) {
|
|
432
|
-
if (!alive.current || mine !== ticket.current) return;
|
|
433
554
|
if (controller.signal.aborted) {
|
|
434
|
-
// The user paused the run themselves. That is not a failure, so
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
setRestore(null);
|
|
440
|
-
setAnnounce(t('state.cancelled'));
|
|
555
|
+
// The user paused the run themselves. That is not a failure, so it
|
|
556
|
+
// must not raise the retry mark: fall back to the resting fish and
|
|
557
|
+
// keep the draft untouched. Screen readers still hear the
|
|
558
|
+
// cancellation through the live region.
|
|
559
|
+
taskStore.patch(sessionId, { failure: null, restore: null, announce: t('state.cancelled') });
|
|
441
560
|
} else {
|
|
442
|
-
|
|
561
|
+
taskStore.patch(sessionId, { failure: error instanceof Error ? error.message : t('state.failed') });
|
|
443
562
|
}
|
|
444
563
|
} finally {
|
|
445
|
-
|
|
446
|
-
if (
|
|
564
|
+
const current = taskStore.ensure(sessionId);
|
|
565
|
+
if (current.controller === controller) taskStore.patch(sessionId, { phase: 'idle', controller: null });
|
|
447
566
|
}
|
|
448
567
|
})();
|
|
449
|
-
}, [draft, draftRev,
|
|
568
|
+
}, [attachmentIds, draft, draftRev, sessionId, sessions, t]);
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Publish this composer as the live delivery target for its session.
|
|
572
|
+
*
|
|
573
|
+
* The task runs outside the component tree, so when it finishes it needs a
|
|
574
|
+
* way to reach whatever editor is on screen RIGHT NOW. Registering the
|
|
575
|
+
* applier here is that bridge: mounted → applied immediately; unmounted →
|
|
576
|
+
* the parked result waits for the next mount, which re-registers and takes it.
|
|
577
|
+
* This replaces the old "effect only runs on mount" delivery, which could not
|
|
578
|
+
* see a result that landed after the first render.
|
|
579
|
+
*/
|
|
580
|
+
useEffect(() => {
|
|
581
|
+
const apply = (text, before, rev) => {
|
|
582
|
+
// Distinguish the two reasons a result may not land, because the user needs
|
|
583
|
+
// to know which happened: the editor refused the write, or the draft moved
|
|
584
|
+
// on while the request was in flight.
|
|
585
|
+
if (latestRev.current !== rev) return 'stale';
|
|
586
|
+
return replaceDraft(text, before, rev) === true ? 'applied' : 'refused';
|
|
587
|
+
};
|
|
588
|
+
taskStore.patch(sessionId, { applyResult: apply });
|
|
589
|
+
return () => {
|
|
590
|
+
// Only clear if we are still the registered applier; a remount may have
|
|
591
|
+
// taken over already.
|
|
592
|
+
const current = taskStore.ensure(sessionId);
|
|
593
|
+
if (current.applyResult === apply) taskStore.patch(sessionId, { applyResult: undefined });
|
|
594
|
+
};
|
|
595
|
+
}, [replaceDraft, sessionId]);
|
|
596
|
+
|
|
597
|
+
// Deliver a parked result: the run finished while this session had no mounted
|
|
598
|
+
// composer (the user was looking at another conversation). Applying it here
|
|
599
|
+
// is what makes "switch away and come back" show a completed rewrite instead
|
|
600
|
+
// of a lost one, and it runs once per result because the record is cleared
|
|
601
|
+
// after consumption.
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
const pendingResult = taskStore.ensure(sessionId).result;
|
|
604
|
+
if (pendingResult === undefined) return;
|
|
605
|
+
// Only the composer that owns this session's live editor may write to it.
|
|
606
|
+
if (inputActions === undefined || typeof inputActions.insertText !== 'function') return;
|
|
607
|
+
const { text, originRev } = pendingResult;
|
|
608
|
+
const swapped = latestRev.current === originRev
|
|
609
|
+
? replaceDraft(text, pendingResult.origin, originRev)
|
|
610
|
+
: false;
|
|
611
|
+
// Consume regardless: a stale result must not be re-applied on every mount.
|
|
612
|
+
taskStore.patch(sessionId, { result: undefined });
|
|
613
|
+
if (!swapped) {
|
|
614
|
+
// The draft moved on while the user was away; keep the outcome visible as
|
|
615
|
+
// a notice instead of overwriting text they have since edited.
|
|
616
|
+
taskStore.patch(sessionId, { restore: null, announce: t('state.stale') });
|
|
617
|
+
}
|
|
618
|
+
}, [inputActions, replaceDraft, sessionId, t]);
|
|
450
619
|
|
|
451
620
|
// Adopt the produced draft on the first render after a successful swap,
|
|
452
621
|
// then retire the offer once the user edits that text.
|
|
453
622
|
useEffect(() => {
|
|
454
623
|
if (restore === null) return;
|
|
455
624
|
if (restore.pending) {
|
|
456
|
-
|
|
625
|
+
taskStore.patch(sessionId, { restore: { text: restore.text, produced: draft, pending: false } });
|
|
457
626
|
return;
|
|
458
627
|
}
|
|
459
|
-
if (restore.produced !== draft)
|
|
460
|
-
}, [draft, restore]);
|
|
628
|
+
if (restore.produced !== draft) taskStore.patch(sessionId, { restore: null });
|
|
629
|
+
}, [draft, restore, sessionId]);
|
|
461
630
|
|
|
462
631
|
if (inputActions === undefined || typeof inputActions.insertText !== 'function') return null;
|
|
463
632
|
|
|
@@ -845,6 +1014,7 @@ window.__ModuleLoader__.load({
|
|
|
845
1014
|
'state.failed': '增强失败,草稿保持不变。',
|
|
846
1015
|
'state.failedWith': '增强失败:{reason}。点击可重试。',
|
|
847
1016
|
'state.cancelled': '已停止增强,草稿保持不变。',
|
|
1017
|
+
'state.stale': '增强已完成,但草稿在你离开期间被改动,结果未写入。',
|
|
848
1018
|
'state.transport': '增强接口不可用(HTTP {status}),请确认插件已启用。',
|
|
849
1019
|
'state.imagesSkipped': '有 {count} 张图片未能读取,本次增强未参考它们。',
|
|
850
1020
|
// Settings page
|
|
@@ -875,6 +1045,7 @@ window.__ModuleLoader__.load({
|
|
|
875
1045
|
'state.failed': 'Enhancement failed; the draft is unchanged.',
|
|
876
1046
|
'state.failedWith': 'Enhancement failed: {reason}. Click to retry.',
|
|
877
1047
|
'state.cancelled': 'Enhancement stopped; the draft is unchanged.',
|
|
1048
|
+
'state.stale': 'The rewrite finished, but the draft changed while you were away, so it was not applied.',
|
|
878
1049
|
'state.transport': 'The enhancement endpoint is unavailable (HTTP {status}); check that the plugin is enabled.',
|
|
879
1050
|
'state.imagesSkipped': '{count} image(s) could not be read; this enhancement did not use them.',
|
|
880
1051
|
// Settings page
|
package/package.json
CHANGED