@restregular/dsh-notepad 0.4.0 → 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/README.md +43 -22
- package/client.js +1043 -166
- package/index.js +52 -3
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +5 -3
package/client.js
CHANGED
|
@@ -37,7 +37,7 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
factory: (require) => {
|
|
38
38
|
const React = require('react');
|
|
39
39
|
const h = React.createElement;
|
|
40
|
-
const { useState, useEffect, useRef } = React;
|
|
40
|
+
const { useState, useEffect, useLayoutEffect, useRef } = React;
|
|
41
41
|
|
|
42
42
|
const NS = 'dsh-notepad';
|
|
43
43
|
const API = '/dsh-notepad/api';
|
|
@@ -56,6 +56,8 @@ window.__ModuleLoader__.load({
|
|
|
56
56
|
const MAX_OPTIMIZE_CHARS = 60000;
|
|
57
57
|
const SIDEBAR_REFRESH_MS = 20000;
|
|
58
58
|
const IMAGE_DELETE_DELAY = 5000;
|
|
59
|
+
const OPTIMIZE_CACHE_LIMIT = 100;
|
|
60
|
+
const EDGE_GAP = 8;
|
|
59
61
|
const IMAGE_MIME = {
|
|
60
62
|
'image/png': 'png',
|
|
61
63
|
'image/jpeg': 'jpg',
|
|
@@ -86,6 +88,10 @@ window.__ModuleLoader__.load({
|
|
|
86
88
|
insert: '插入到输入框',
|
|
87
89
|
insertSegment: '插入此段',
|
|
88
90
|
insertSelected: '插入选中的 {n} 段',
|
|
91
|
+
insertNewSession: '在新会话中插入',
|
|
92
|
+
insertNewSelected: '在新会话中插入选中的 {n} 段',
|
|
93
|
+
insertNewDone: '已在新会话中填入内容',
|
|
94
|
+
insertNewUnavailable: '无法新建会话,内容已复制',
|
|
89
95
|
inserted: '已插入输入框',
|
|
90
96
|
insertEmpty: '没有可插入的内容',
|
|
91
97
|
insertUnavailable: '输入框暂不可用,内容已复制',
|
|
@@ -97,6 +103,10 @@ window.__ModuleLoader__.load({
|
|
|
97
103
|
send: '发送',
|
|
98
104
|
sendSegment: '发送此段',
|
|
99
105
|
sendSelected: '发送选中的 {n} 段',
|
|
106
|
+
sendNewSession: '在新会话中发送',
|
|
107
|
+
sendNewSelected: '在新会话中发送选中的 {n} 段',
|
|
108
|
+
sendNewDone: '已在新会话中发送',
|
|
109
|
+
sendNewUnavailable: '无法新建会话,内容已复制',
|
|
100
110
|
sending: '发送中…',
|
|
101
111
|
sent: '已发送',
|
|
102
112
|
sendFailed: '发送失败',
|
|
@@ -114,11 +124,14 @@ window.__ModuleLoader__.load({
|
|
|
114
124
|
mergeHint: '至少选择 2 段才能合并',
|
|
115
125
|
mergedToast: '已合并 {n} 段',
|
|
116
126
|
optimizeSegment: 'AI 优化此段',
|
|
117
|
-
|
|
127
|
+
optimizeSelected: 'AI 优化选中的 {n} 段',
|
|
128
|
+
optimizeSelectHint: '勾选分段后可批量优化',
|
|
118
129
|
optimizeAllProgress: 'AI 优化中 {done}/{total}',
|
|
119
130
|
optimizing: '优化中…',
|
|
131
|
+
undoOptimize: '撤销优化',
|
|
120
132
|
optimizedToast: '已优化此段',
|
|
121
133
|
optimizedCount: '已优化 {n} 段',
|
|
134
|
+
optimizeUndone: '已撤销优化',
|
|
122
135
|
optimizeEmpty: '没有可优化的文字',
|
|
123
136
|
optimizeTooLong: '内容太长,无法优化',
|
|
124
137
|
optimizeUnavailable: 'AI 服务不可用',
|
|
@@ -142,6 +155,12 @@ window.__ModuleLoader__.load({
|
|
|
142
155
|
restoredToast: '已恢复',
|
|
143
156
|
restoredCount: '已恢复 {n} 段',
|
|
144
157
|
deleteSegment: '删除此段',
|
|
158
|
+
deleteActiveSegment: '删除此段(不可恢复)',
|
|
159
|
+
deleteConfirmTitle: '删除这个分段?',
|
|
160
|
+
deleteConfirmBody: '删除后无法恢复。分段里的文字和图片引用会一起移除。',
|
|
161
|
+
deleteConfirmHint: '此操作不可撤销',
|
|
162
|
+
deleteConfirmCancel: '取消',
|
|
163
|
+
deleteConfirmOk: '删除',
|
|
145
164
|
deletedToast: '已删除',
|
|
146
165
|
clearArchive: '清空归档',
|
|
147
166
|
clearedArchive: '已清空 {n} 段归档',
|
|
@@ -155,12 +174,24 @@ window.__ModuleLoader__.load({
|
|
|
155
174
|
show: '显示记事本(Ctrl+Shift+N)',
|
|
156
175
|
chars: '{n} 字符',
|
|
157
176
|
segments: '{n} 段',
|
|
158
|
-
|
|
177
|
+
hintNext: '点击查看下一条快捷键',
|
|
178
|
+
hintEnter: 'Enter 新建分段',
|
|
179
|
+
hintNewline: 'Shift+Enter 分段内换行',
|
|
180
|
+
hintInsert: 'Ctrl+Enter 插入当前分段',
|
|
181
|
+
hintToggle: 'Ctrl+Shift+N 显示 / 隐藏浮窗',
|
|
182
|
+
hintReset: 'Ctrl+Shift+0 复位浮窗',
|
|
183
|
+
hintArrow: '↑ / ↓ 切换分段',
|
|
184
|
+
hintPaste: '粘贴图片自动保存到工作区',
|
|
185
|
+
resetShortcut: '复位记事本位置和尺寸',
|
|
159
186
|
workspace: '工作区',
|
|
160
187
|
resize: '拖动调整大小',
|
|
188
|
+
uiReset: '已复位浮窗位置和尺寸',
|
|
161
189
|
addImage: '添加图片',
|
|
162
190
|
removeImage: '移除图片',
|
|
163
191
|
openImage: '打开图片',
|
|
192
|
+
imagePreview: '图片预览',
|
|
193
|
+
closePreview: '关闭预览',
|
|
194
|
+
openOriginal: '打开原图',
|
|
164
195
|
imageAdded: '已添加 {n} 张图片',
|
|
165
196
|
imageFailed: '图片添加失败',
|
|
166
197
|
imageUnsupported: '只支持 PNG / JPEG / WebP / GIF 图片',
|
|
@@ -190,6 +221,10 @@ window.__ModuleLoader__.load({
|
|
|
190
221
|
insert: 'Insert into composer',
|
|
191
222
|
insertSegment: 'Insert this segment',
|
|
192
223
|
insertSelected: 'Insert {n} selected',
|
|
224
|
+
insertNewSession: 'Insert into a new session',
|
|
225
|
+
insertNewSelected: 'Insert {n} selected into a new session',
|
|
226
|
+
insertNewDone: 'Filled into a new session',
|
|
227
|
+
insertNewUnavailable: 'Could not create a session; copied instead',
|
|
193
228
|
inserted: 'Inserted into composer',
|
|
194
229
|
insertEmpty: 'Nothing to insert',
|
|
195
230
|
insertUnavailable: 'Composer unavailable; copied instead',
|
|
@@ -201,6 +236,10 @@ window.__ModuleLoader__.load({
|
|
|
201
236
|
send: 'Send',
|
|
202
237
|
sendSegment: 'Send this segment',
|
|
203
238
|
sendSelected: 'Send {n} selected',
|
|
239
|
+
sendNewSession: 'Send into a new session',
|
|
240
|
+
sendNewSelected: 'Send {n} selected into a new session',
|
|
241
|
+
sendNewDone: 'Sent into a new session',
|
|
242
|
+
sendNewUnavailable: 'Could not create a session; copied instead',
|
|
204
243
|
sending: 'Sending…',
|
|
205
244
|
sent: 'Sent',
|
|
206
245
|
sendFailed: 'Send failed',
|
|
@@ -218,11 +257,14 @@ window.__ModuleLoader__.load({
|
|
|
218
257
|
mergeHint: 'Select at least 2 segments to merge',
|
|
219
258
|
mergedToast: 'Merged {n} segments',
|
|
220
259
|
optimizeSegment: 'Improve this segment with AI',
|
|
221
|
-
|
|
260
|
+
optimizeSelected: 'Improve {n} selected with AI',
|
|
261
|
+
optimizeSelectHint: 'Select segments to improve',
|
|
222
262
|
optimizeAllProgress: 'Improving {done}/{total}',
|
|
223
263
|
optimizing: 'Improving…',
|
|
264
|
+
undoOptimize: 'Undo improvement',
|
|
224
265
|
optimizedToast: 'Segment improved',
|
|
225
266
|
optimizedCount: 'Improved {n} segments',
|
|
267
|
+
optimizeUndone: 'Improvement undone',
|
|
226
268
|
optimizeEmpty: 'Nothing to improve',
|
|
227
269
|
optimizeTooLong: 'Too long to improve',
|
|
228
270
|
optimizeUnavailable: 'AI service unavailable',
|
|
@@ -246,6 +288,12 @@ window.__ModuleLoader__.load({
|
|
|
246
288
|
restoredToast: 'Restored',
|
|
247
289
|
restoredCount: 'Restored {n}',
|
|
248
290
|
deleteSegment: 'Delete this segment',
|
|
291
|
+
deleteActiveSegment: 'Delete this segment (cannot be undone)',
|
|
292
|
+
deleteConfirmTitle: 'Delete this segment?',
|
|
293
|
+
deleteConfirmBody: 'This cannot be undone. The text and image references in the segment will be removed too.',
|
|
294
|
+
deleteConfirmHint: 'This action cannot be undone',
|
|
295
|
+
deleteConfirmCancel: 'Cancel',
|
|
296
|
+
deleteConfirmOk: 'Delete',
|
|
249
297
|
deletedToast: 'Deleted',
|
|
250
298
|
clearArchive: 'Clear archive',
|
|
251
299
|
clearedArchive: 'Cleared {n} archived',
|
|
@@ -259,12 +307,24 @@ window.__ModuleLoader__.load({
|
|
|
259
307
|
show: 'Show notepad (Ctrl+Shift+N)',
|
|
260
308
|
chars: '{n} chars',
|
|
261
309
|
segments: '{n} segments',
|
|
262
|
-
|
|
310
|
+
hintNext: 'Click for the next shortcut',
|
|
311
|
+
hintEnter: 'Enter creates a segment',
|
|
312
|
+
hintNewline: 'Shift+Enter adds a line break',
|
|
313
|
+
hintInsert: 'Ctrl+Enter inserts the current segment',
|
|
314
|
+
hintToggle: 'Ctrl+Shift+N shows / hides the notepad',
|
|
315
|
+
hintReset: 'Ctrl+Shift+0 resets the floating window',
|
|
316
|
+
hintArrow: '↑ / ↓ switch segments',
|
|
317
|
+
hintPaste: 'Pasted images are saved in the workspace',
|
|
318
|
+
resetShortcut: 'Reset the notepad position and size',
|
|
263
319
|
workspace: 'Workspace',
|
|
264
320
|
resize: 'Drag to resize',
|
|
321
|
+
uiReset: 'Window position and size reset',
|
|
265
322
|
addImage: 'Add image',
|
|
266
323
|
removeImage: 'Remove image',
|
|
267
324
|
openImage: 'Open image',
|
|
325
|
+
imagePreview: 'Image preview',
|
|
326
|
+
closePreview: 'Close preview',
|
|
327
|
+
openOriginal: 'Open original',
|
|
268
328
|
imageAdded: 'Added {n} image(s)',
|
|
269
329
|
imageFailed: 'Could not add image',
|
|
270
330
|
imageUnsupported: 'Only PNG / JPEG / WebP / GIF images are supported',
|
|
@@ -277,6 +337,17 @@ window.__ModuleLoader__.load({
|
|
|
277
337
|
},
|
|
278
338
|
};
|
|
279
339
|
|
|
340
|
+
/** Rotating shortcut tips shown one at a time in the footer. */
|
|
341
|
+
const HINT_KEYS = [
|
|
342
|
+
'hintEnter',
|
|
343
|
+
'hintNewline',
|
|
344
|
+
'hintInsert',
|
|
345
|
+
'hintToggle',
|
|
346
|
+
'hintReset',
|
|
347
|
+
'hintArrow',
|
|
348
|
+
'hintPaste',
|
|
349
|
+
];
|
|
350
|
+
|
|
280
351
|
function preferredLanguage() {
|
|
281
352
|
try {
|
|
282
353
|
const tag = String((typeof navigator !== 'undefined' && navigator.language) || 'zh').toLowerCase();
|
|
@@ -548,6 +619,78 @@ window.__ModuleLoader__.load({
|
|
|
548
619
|
);
|
|
549
620
|
}
|
|
550
621
|
|
|
622
|
+
/** Curved arrow: undo the last AI improvement. */
|
|
623
|
+
function UndoIcon({ size = 16 }) {
|
|
624
|
+
return h(
|
|
625
|
+
Svg,
|
|
626
|
+
{ size, viewBox: '0 0 16 16' },
|
|
627
|
+
h('path', {
|
|
628
|
+
d: 'M5.4 4.2 2.8 6.8l2.6 2.6',
|
|
629
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
630
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
631
|
+
}),
|
|
632
|
+
h('path', {
|
|
633
|
+
d: 'M3 6.8h5.3c2.7 0 4.9 2.2 4.9 4.9',
|
|
634
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
635
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
636
|
+
}),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Two panels, the second with a plus: start a new session. */
|
|
641
|
+
function NewSessionIcon({ size = 16 }) {
|
|
642
|
+
return h(
|
|
643
|
+
Svg,
|
|
644
|
+
{ size, viewBox: '0 0 16 16' },
|
|
645
|
+
h('rect', {
|
|
646
|
+
x: 1.9, y: 3.2, width: 5.1, height: 9.4, rx: 1.4,
|
|
647
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
648
|
+
}),
|
|
649
|
+
h('rect', {
|
|
650
|
+
x: 8.7, y: 3.2, width: 5.4, height: 9.4, rx: 1.4,
|
|
651
|
+
stroke: 'currentColor', strokeWidth: 1.25,
|
|
652
|
+
}),
|
|
653
|
+
h('path', {
|
|
654
|
+
d: 'M11.4 6.1v3.8M9.5 8h3.8',
|
|
655
|
+
stroke: 'currentColor', strokeWidth: 1.25, strokeLinecap: 'round',
|
|
656
|
+
}),
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** Circular arrow: reset the floating window geometry. */
|
|
661
|
+
function ResetIcon({ size = 16 }) {
|
|
662
|
+
return h(
|
|
663
|
+
Svg,
|
|
664
|
+
{ size, viewBox: '0 0 16 16' },
|
|
665
|
+
h('path', {
|
|
666
|
+
d: 'M13 8a5 5 0 1 1-1.6-3.7',
|
|
667
|
+
stroke: 'currentColor', strokeWidth: 1.35, strokeLinecap: 'round',
|
|
668
|
+
}),
|
|
669
|
+
h('path', {
|
|
670
|
+
d: 'M13.3 2.7v3.4H9.9',
|
|
671
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
672
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
673
|
+
}),
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Arrow leaving a frame: open the original image. */
|
|
678
|
+
function OpenIcon({ size = 16 }) {
|
|
679
|
+
return h(
|
|
680
|
+
Svg,
|
|
681
|
+
{ size, viewBox: '0 0 16 16' },
|
|
682
|
+
h('path', {
|
|
683
|
+
d: 'M9.4 3.2h3.4v3.4M12.8 3.2 7.6 8.4',
|
|
684
|
+
stroke: 'currentColor', strokeWidth: 1.3,
|
|
685
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
686
|
+
}),
|
|
687
|
+
h('path', {
|
|
688
|
+
d: 'M12.4 9.4v2.3c0 .7-.6 1.3-1.3 1.3H4.6c-.7 0-1.3-.6-1.3-1.3V5.2c0-.7.6-1.3 1.3-1.3h2.3',
|
|
689
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
551
694
|
/** Two arrows converging: merge selected segments into one. */
|
|
552
695
|
function MergeIcon({ size = 16 }) {
|
|
553
696
|
return h(
|
|
@@ -608,7 +751,7 @@ window.__ModuleLoader__.load({
|
|
|
608
751
|
.dsh-np-btn[data-accent=true]{background:var(--np-accent);color:var(--np-accent-fg)}
|
|
609
752
|
.dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 88%,#000);color:var(--np-accent-fg)}
|
|
610
753
|
.dsh-np-btn:disabled{opacity:.38;cursor:default;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280)}
|
|
611
|
-
.dsh-np-btn:focus-visible,.dsh-np-segbtn:focus-visible,.dsh-np-link:focus-visible,.dsh-np-empty-add:focus-visible,.dsh-np-toast-action:focus-visible,.dsh-np-selectall input:focus-visible,.dsh-np-check input:focus-visible,.dsh-np-restore:focus-visible,.dsh-np-img-open:focus-visible,.dsh-np-img-remove:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4176e6));outline-offset:-2px}
|
|
754
|
+
.dsh-np-btn:focus-visible,.dsh-np-segbtn:focus-visible,.dsh-np-link:focus-visible,.dsh-np-empty-add:focus-visible,.dsh-np-toast-action:focus-visible,.dsh-np-selectall input:focus-visible,.dsh-np-check input:focus-visible,.dsh-np-title-add:focus-visible,.dsh-np-preview-btn:focus-visible,.dsh-np-confirm-btn:focus-visible,button.dsh-np-hint:focus-visible,.dsh-np-restore:focus-visible,.dsh-np-img-open:focus-visible,.dsh-np-img-remove:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4176e6));outline-offset:-2px}
|
|
612
755
|
.dsh-np-body{display:flex;flex:1 1 auto;flex-direction:column;min-height:0;padding:8px 10px 0;opacity:1;transition:opacity .16s ease,visibility 0s linear 0s}
|
|
613
756
|
.dsh-np-card[data-minimized=true] .dsh-np-body{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,visibility 0s linear .16s}
|
|
614
757
|
.dsh-np-tools{display:flex;align-items:center;flex-wrap:wrap;gap:4px;flex:none;padding:0 2px 7px;border-bottom:1px solid var(--dsw-alias-border-l1,rgb(15 23 42/.08))}
|
|
@@ -633,6 +776,7 @@ window.__ModuleLoader__.load({
|
|
|
633
776
|
.dsh-np-title-input::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2);font-weight:400}
|
|
634
777
|
.dsh-np-title-input:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06))}
|
|
635
778
|
.dsh-np-title-input:focus{background:var(--np-surface);box-shadow:0 0 0 1px var(--np-accent) inset;color:var(--dsw-alias-label-primary,#1f2430)}
|
|
779
|
+
.dsh-np-title-add{flex:1 1 auto;min-width:0;width:0;height:20px;padding:0 4px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-caption,#9aa3b2);font:inherit;font-size:11px;font-weight:400;line-height:20px;text-align:left;cursor:text;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.75}
|
|
636
780
|
.dsh-np-seg-title{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,#4b5563);font-size:11px;font-weight:600}
|
|
637
781
|
.dsh-np-archived-chip{flex:none;padding:0 6px;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-tertiary,#6b7280);font-size:10px;line-height:16px}
|
|
638
782
|
.dsh-np-segbtn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;transition:background .12s,color .12s,opacity .12s;opacity:.8}
|
|
@@ -640,6 +784,11 @@ window.__ModuleLoader__.load({
|
|
|
640
784
|
.dsh-np-segbtn[data-ai=true]{color:var(--np-accent);opacity:1}
|
|
641
785
|
.dsh-np-segbtn[data-danger=true]:hover:not(:disabled){color:var(--dsw-alias-state-error-primary,#e25555);background:var(--dsw-alias-interactive-bg-hover-danger,rgb(236 19 19/.05))}
|
|
642
786
|
.dsh-np-segbtn:disabled{opacity:.3;cursor:default}
|
|
787
|
+
.dsh-np-actiongroup{display:inline-flex;align-items:center;flex:none;padding:1px;border:1px solid transparent;border-radius:8px;transition:border-color .12s,background .12s}
|
|
788
|
+
.dsh-np-actiongroup:hover,.dsh-np-actiongroup:focus-within{border-color:var(--dsw-alias-border-l2,rgb(15 23 42/.14));background:color-mix(in srgb,var(--np-surface-2) 58%,transparent)}
|
|
789
|
+
.dsh-np-group-extra{width:0!important;min-width:0;padding:0;border:0;opacity:0;visibility:hidden;pointer-events:none;overflow:hidden;transition:width .14s ease,opacity .12s ease,visibility 0s linear .14s}
|
|
790
|
+
.dsh-np-actiongroup:hover .dsh-np-group-extra,.dsh-np-actiongroup:focus-within .dsh-np-group-extra{width:24px!important;opacity:1;visibility:visible;pointer-events:auto;transition:width .14s ease,opacity .12s ease,visibility 0s linear 0s}
|
|
791
|
+
.dsh-np-actiongroup[data-size=bar]:hover .dsh-np-group-extra,.dsh-np-actiongroup[data-size=bar]:focus-within .dsh-np-group-extra{width:27px!important}
|
|
643
792
|
.dsh-np-seg-text{display:block;width:100%;box-sizing:border-box;padding:2px 10px 8px;border:0;outline:0;resize:none;background:transparent;color:inherit;font-family:${FONT_STACK};font-size:13px;line-height:1.6;min-height:26px;max-height:240px;overflow-y:auto;white-space:pre-wrap;overflow-wrap:anywhere;tab-size:2}
|
|
644
793
|
.dsh-np-seg-text::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2)}
|
|
645
794
|
.dsh-np-seg-text:disabled{opacity:.6}
|
|
@@ -677,6 +826,24 @@ window.__ModuleLoader__.load({
|
|
|
677
826
|
.dsh-np-retry{flex:none;padding:0;border:0;background:none;color:var(--dsw-alias-link,#4176e6);font:inherit;font-size:10.5px;cursor:pointer;text-decoration:underline}
|
|
678
827
|
.dsh-np-count{flex:none;font-variant-numeric:tabular-nums}
|
|
679
828
|
.dsh-np-hint{margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.85;font-size:10px}
|
|
829
|
+
button.dsh-np-hint{padding:0;border:0;background:none;color:inherit;font:inherit;font-size:10px;cursor:pointer;text-align:right}
|
|
830
|
+
button.dsh-np-hint:hover{opacity:1;color:var(--dsw-alias-label-secondary,#4b5563);text-decoration:underline}
|
|
831
|
+
.dsh-np-preview{position:absolute;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;padding:44px 20px 28px;background:rgb(8 12 20/.74);backdrop-filter:blur(3px);animation:dsh-np-fade .14s ease-out}
|
|
832
|
+
.dsh-np-preview-img{display:block;max-width:100%;max-height:100%;object-fit:contain;border-radius:8px;background:#fff;box-shadow:0 16px 40px rgb(0 0 0/.35)}
|
|
833
|
+
.dsh-np-preview-bar{position:absolute;top:8px;right:8px;display:flex;align-items:center;gap:4px}
|
|
834
|
+
.dsh-np-preview-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid rgb(255 255 255/.18);border-radius:7px;background:rgb(15 23 42/.55);color:#f8fafc;cursor:pointer;backdrop-filter:blur(4px)}
|
|
835
|
+
.dsh-np-preview-btn:hover{background:rgb(15 23 42/.82)}
|
|
836
|
+
.dsh-np-preview-name{position:absolute;left:12px;bottom:8px;max-width:calc(100% - 24px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#e5e7eb;font-size:11px}
|
|
837
|
+
.dsh-np-confirm{position:absolute;inset:0;z-index:21;display:flex;align-items:center;justify-content:center;padding:16px;background:rgb(8 12 20/.55);backdrop-filter:blur(2px);animation:dsh-np-fade .14s ease-out}
|
|
838
|
+
.dsh-np-confirm-card{width:min(100%,300px);padding:14px;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.14));border-radius:12px;background:var(--np-surface);box-shadow:var(--dsw-elevation-prominent,0 20px 50px rgb(15 23 42/.22))}
|
|
839
|
+
.dsh-np-confirm-title{font-size:13px;font-weight:650;color:var(--dsw-alias-label-primary,#1f2430)}
|
|
840
|
+
.dsh-np-confirm-body{margin-top:6px;color:var(--dsw-alias-label-secondary,#4b5563);font-size:12px;line-height:1.55}
|
|
841
|
+
.dsh-np-confirm-hint{margin-top:8px;color:var(--dsw-alias-state-error-primary,#e25555);font-size:11px}
|
|
842
|
+
.dsh-np-confirm-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:12px}
|
|
843
|
+
.dsh-np-confirm-btn{height:28px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.14));border-radius:7px;background:var(--np-surface-2);color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:12px;cursor:pointer}
|
|
844
|
+
.dsh-np-confirm-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430)}
|
|
845
|
+
.dsh-np-confirm-btn[data-danger=true]{border-color:transparent;background:var(--dsw-alias-state-error-primary,#e25555);color:#fff}
|
|
846
|
+
.dsh-np-confirm-btn[data-danger=true]:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e25555) 86%,#000);color:#fff}
|
|
680
847
|
.dsh-np-toast{position:absolute;left:50%;bottom:38px;transform:translateX(-50%);z-index:2;display:flex;align-items:center;gap:8px;max-width:calc(100% - 32px);padding:5px 10px;border-radius:8px;background:var(--np-surface-2);border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));box-shadow:var(--dsw-elevation-lv2,0 8px 24px rgb(15 23 42/.18));color:var(--dsw-alias-label-primary,#1f2430);font-size:11.5px;white-space:nowrap;overflow:hidden;animation:dsh-np-rise .16s ease-out}
|
|
681
848
|
.dsh-np-toast-text{overflow:hidden;text-overflow:ellipsis}
|
|
682
849
|
.dsh-np-toast-action{flex:none;padding:0;border:0;background:none;color:var(--dsw-alias-link,#4176e6);font:inherit;font-size:11.5px;font-weight:600;cursor:pointer;text-decoration:underline}
|
|
@@ -691,6 +858,7 @@ window.__ModuleLoader__.load({
|
|
|
691
858
|
[data-row-key^="workspace:"][data-np-unsent]{position:relative}
|
|
692
859
|
[data-row-key^="workspace:"][data-np-unsent]::after{content:attr(data-np-unsent);position:absolute;top:-4px;left:1px;z-index:2;min-width:15px;height:15px;padding:0 4px;box-sizing:border-box;border-radius:8px;background:var(--dsw-alias-state-business-primary,#4176e6);color:#fff;font-size:9.5px;font-weight:700;line-height:15px;text-align:center;font-variant-numeric:tabular-nums;pointer-events:none;box-shadow:0 0 0 1.5px var(--dsw-specific-sidebar-fill,var(--dsw-alias-bg-layer-1,#fff))}
|
|
693
860
|
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
861
|
+
@keyframes dsh-np-fade{from{opacity:0}to{opacity:1}}
|
|
694
862
|
@keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
695
863
|
@keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
|
|
696
864
|
@keyframes dsh-np-spin{to{transform:rotate(360deg)}}
|
|
@@ -698,7 +866,7 @@ body[data-ds-dark-theme] .dsh-np-card{--np-accent-fg:#0f1115;box-shadow:0 18px 4
|
|
|
698
866
|
body[data-ds-dark-theme] .dsh-np-seg[data-selected=true]{background:color-mix(in srgb,var(--np-accent) 14%,var(--dsw-alias-bg-layer-2,#2c2c2e))}
|
|
699
867
|
body[data-ds-dark-theme] .dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 84%,#fff)}
|
|
700
868
|
body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
701
|
-
@media (prefers-reduced-motion:reduce){.dsh-np-card,.dsh-np-body,.dsh-np-archive,.dsh-np-restore,.dsh-np-seg,.dsh-np-toast{transition:none!important;animation:none!important}.dsh-np-dot[data-state=saving]{animation:none}.dsh-np-img-spinner,.dsh-np-mini-spinner{animation:none}}`;
|
|
869
|
+
@media (prefers-reduced-motion:reduce){.dsh-np-card,.dsh-np-body,.dsh-np-archive,.dsh-np-restore,.dsh-np-seg,.dsh-np-toast,.dsh-np-actiongroup,.dsh-np-group-extra,.dsh-np-preview,.dsh-np-confirm{transition:none!important;animation:none!important}.dsh-np-dot[data-state=saving]{animation:none}.dsh-np-img-spinner,.dsh-np-mini-spinner{animation:none}}`;
|
|
702
870
|
|
|
703
871
|
// ------------------------------------------------------------------
|
|
704
872
|
// Helpers
|
|
@@ -1032,6 +1200,43 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1032
1200
|
return parts.length === 0 ? String(root ?? '') : parts[parts.length - 1];
|
|
1033
1201
|
}
|
|
1034
1202
|
|
|
1203
|
+
/**
|
|
1204
|
+
* Height of the chrome the host draws over the page (the Windows title bar
|
|
1205
|
+
* with maximize / minimize / close, or the macOS traffic lights). The
|
|
1206
|
+
* floating card must never move above that strip. The host exposes it as a
|
|
1207
|
+
* CSS variable on <html>; standalone browsers have none, so this is 0.
|
|
1208
|
+
* @returns {number}
|
|
1209
|
+
*/
|
|
1210
|
+
function hostChromeTop() {
|
|
1211
|
+
try {
|
|
1212
|
+
const root = typeof document !== 'undefined' ? document.documentElement : null;
|
|
1213
|
+
if (root === null || root === undefined) return 0;
|
|
1214
|
+
const hasAttribute = (name) => typeof root.hasAttribute === 'function' && root.hasAttribute(name);
|
|
1215
|
+
const readVar = (name) => {
|
|
1216
|
+
try {
|
|
1217
|
+
if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return NaN;
|
|
1218
|
+
const style = window.getComputedStyle(root);
|
|
1219
|
+
return Number.parseFloat(style?.getPropertyValue(name) ?? '');
|
|
1220
|
+
} catch {
|
|
1221
|
+
return NaN;
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
// `--dsh-frame-chrome-top` is 0 while the host is in fullscreen.
|
|
1225
|
+
const chrome = readVar('--dsh-frame-chrome-top');
|
|
1226
|
+
if (Number.isFinite(chrome)) return Math.max(0, chrome);
|
|
1227
|
+
const clearance = readVar('--dsh-frame-top-clearance');
|
|
1228
|
+
if (Number.isFinite(clearance) && clearance > 0) return clearance;
|
|
1229
|
+
if (hasAttribute('data-windows-titlebar')) {
|
|
1230
|
+
const titlebar = readVar('--dsh-windows-titlebar-height');
|
|
1231
|
+
return Number.isFinite(titlebar) && titlebar > 0 ? titlebar : 32;
|
|
1232
|
+
}
|
|
1233
|
+
const platform = typeof root.getAttribute === 'function' ? root.getAttribute('data-platform') : '';
|
|
1234
|
+
return platform === 'darwin' ? 48 : 0;
|
|
1235
|
+
} catch {
|
|
1236
|
+
return 0;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1035
1240
|
function readStoredUi() {
|
|
1036
1241
|
const base = {
|
|
1037
1242
|
x: null,
|
|
@@ -1064,17 +1269,18 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1064
1269
|
function clampUi(ui) {
|
|
1065
1270
|
const vw = (typeof window !== 'undefined' && window.innerWidth) || 1280;
|
|
1066
1271
|
const vh = (typeof window !== 'undefined' && window.innerHeight) || 800;
|
|
1067
|
-
const
|
|
1068
|
-
const
|
|
1272
|
+
const minTop = hostChromeTop() + EDGE_GAP;
|
|
1273
|
+
const widthLimit = Math.max(240, vw - EDGE_GAP * 2);
|
|
1274
|
+
const heightLimit = Math.max(180, vh - minTop - EDGE_GAP);
|
|
1069
1275
|
const width = Math.min(Math.max(MIN_W, Number(ui.width) || DEFAULT_W), widthLimit);
|
|
1070
1276
|
const height = Math.min(Math.max(MIN_H, Number(ui.height) || DEFAULT_H), heightLimit);
|
|
1071
1277
|
const displayHeight = ui.minimized ? HEAD_H : height;
|
|
1072
1278
|
const x = ui.x === null || ui.x === undefined
|
|
1073
|
-
? vw - width - 24
|
|
1074
|
-
: Math.min(Math.max(
|
|
1279
|
+
? Math.max(EDGE_GAP, vw - width - 24)
|
|
1280
|
+
: Math.min(Math.max(EDGE_GAP, ui.x), Math.max(EDGE_GAP, vw - width - EDGE_GAP));
|
|
1075
1281
|
const y = ui.y === null || ui.y === undefined
|
|
1076
|
-
? vh - displayHeight - 24
|
|
1077
|
-
: Math.min(Math.max(
|
|
1282
|
+
? Math.max(minTop, vh - displayHeight - 24)
|
|
1283
|
+
: Math.min(Math.max(minTop, ui.y), Math.max(minTop, vh - displayHeight - EDGE_GAP));
|
|
1078
1284
|
return { ...ui, x, y, width, height };
|
|
1079
1285
|
}
|
|
1080
1286
|
|
|
@@ -1185,6 +1391,38 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1185
1391
|
}
|
|
1186
1392
|
}
|
|
1187
1393
|
|
|
1394
|
+
/** Mounted notepads register here so a host shortcut can reset them. */
|
|
1395
|
+
const resetHandlers = new Set();
|
|
1396
|
+
|
|
1397
|
+
/** Whether the host shortcut service already owns Ctrl/Cmd+Shift+0. */
|
|
1398
|
+
let resetShortcutHandledByHost = false;
|
|
1399
|
+
|
|
1400
|
+
/** Ask every mounted notepad to reset its position and size. */
|
|
1401
|
+
function requestUiReset() {
|
|
1402
|
+
let handled = false;
|
|
1403
|
+
for (const handler of resetHandlers) {
|
|
1404
|
+
try {
|
|
1405
|
+
handler();
|
|
1406
|
+
handled = true;
|
|
1407
|
+
} catch {
|
|
1408
|
+
/* One failing overlay must not block the others. */
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return handled;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/** Read the General Settings "send behavior while busy" preference. */
|
|
1415
|
+
function busyEnterPreference(ctx) {
|
|
1416
|
+
try {
|
|
1417
|
+
const forms = ctx.get('configForms') ?? ctx.configForms;
|
|
1418
|
+
const form = typeof forms?.get === 'function' ? forms.get('ui-conversation') : undefined;
|
|
1419
|
+
const value = form?.getSnapshot?.()?.value;
|
|
1420
|
+
return value?.busyEnter === 'steer' ? 'steer' : 'queue';
|
|
1421
|
+
} catch {
|
|
1422
|
+
return 'queue';
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1188
1426
|
// ------------------------------------------------------------------
|
|
1189
1427
|
// Overlay
|
|
1190
1428
|
// ------------------------------------------------------------------
|
|
@@ -1201,7 +1439,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1201
1439
|
const [error, setError] = useState('');
|
|
1202
1440
|
const [savedAt, setSavedAt] = useState(null);
|
|
1203
1441
|
const [toast, setToast] = useState(null);
|
|
1204
|
-
const [ui, setUi] = useState(readStoredUi);
|
|
1442
|
+
const [ui, setUi] = useState(() => clampUi(readStoredUi()));
|
|
1205
1443
|
const [sending, setSending] = useState(false);
|
|
1206
1444
|
const [resizing, setResizing] = useState(false);
|
|
1207
1445
|
const [uploading, setUploading] = useState({});
|
|
@@ -1209,7 +1447,13 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1209
1447
|
const [reorderDrag, setReorderDrag] = useState('');
|
|
1210
1448
|
const [reorderOver, setReorderOver] = useState(null);
|
|
1211
1449
|
const [optimizing, setOptimizing] = useState(() => new Set());
|
|
1212
|
-
const [
|
|
1450
|
+
const [optimizeBatch, setOptimizeBatch] = useState(null);
|
|
1451
|
+
const [segmentUndo, setSegmentUndo] = useState(() => new Map());
|
|
1452
|
+
const [batchUndo, setBatchUndo] = useState(null);
|
|
1453
|
+
const [titleEditing, setTitleEditing] = useState('');
|
|
1454
|
+
const [previewImage, setPreviewImage] = useState(null);
|
|
1455
|
+
const [deleteConfirm, setDeleteConfirm] = useState('');
|
|
1456
|
+
const [hintIndex, setHintIndex] = useState(0);
|
|
1213
1457
|
const [wsCounts, setWsCounts] = useState({});
|
|
1214
1458
|
const [currentWsId, setCurrentWsId] = useState('');
|
|
1215
1459
|
|
|
@@ -1235,9 +1479,13 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1235
1479
|
const fileInputRefs = useRef(new Map());
|
|
1236
1480
|
const listRef = useRef(null);
|
|
1237
1481
|
const pendingFocusRef = useRef(null);
|
|
1482
|
+
const pendingScrollRef = useRef(null);
|
|
1483
|
+
const refreshSeqRef = useRef(0);
|
|
1238
1484
|
const undoRef = useRef(null);
|
|
1239
1485
|
const optimizingRef = useRef(new Set());
|
|
1240
|
-
const
|
|
1486
|
+
const optimizeBatchRef = useRef(false);
|
|
1487
|
+
const optimizeCacheRef = useRef(new Map());
|
|
1488
|
+
const batchUndoRef = useRef(null);
|
|
1241
1489
|
const reorderDragRef = useRef('');
|
|
1242
1490
|
const reorderOverRef = useRef(null);
|
|
1243
1491
|
const imageDeleteTimersRef = useRef(new Map());
|
|
@@ -1259,6 +1507,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1259
1507
|
if (changed) setSelection(pruned);
|
|
1260
1508
|
};
|
|
1261
1509
|
|
|
1510
|
+
/** Keep the ref and the state in sync for the batch-optimize undo slot. */
|
|
1511
|
+
const setBatchUndoState = (value) => {
|
|
1512
|
+
batchUndoRef.current = value;
|
|
1513
|
+
setBatchUndo(value);
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
/** Any local edit invalidates the "undo the last batch" offer. */
|
|
1517
|
+
const invalidateBatchUndo = () => {
|
|
1518
|
+
if (batchUndoRef.current !== null) setBatchUndoState(null);
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1262
1521
|
// Session + workspace -------------------------------------------------
|
|
1263
1522
|
useEffect(() => {
|
|
1264
1523
|
let disposed = false;
|
|
@@ -1335,6 +1594,24 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1335
1594
|
toastTimerRef.current = setTimeout(() => setToast(null), action === undefined || action === null ? 1800 : 5000);
|
|
1336
1595
|
};
|
|
1337
1596
|
|
|
1597
|
+
const resetGeometry = () => {
|
|
1598
|
+
setUi((previous) => clampUi({
|
|
1599
|
+
...previous,
|
|
1600
|
+
x: null,
|
|
1601
|
+
y: null,
|
|
1602
|
+
width: DEFAULT_W,
|
|
1603
|
+
height: DEFAULT_H,
|
|
1604
|
+
}));
|
|
1605
|
+
showToast(t('uiReset'));
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
useEffect(() => {
|
|
1609
|
+
resetHandlers.add(resetGeometry);
|
|
1610
|
+
return () => {
|
|
1611
|
+
resetHandlers.delete(resetGeometry);
|
|
1612
|
+
};
|
|
1613
|
+
}, []);
|
|
1614
|
+
|
|
1338
1615
|
const scheduleSave = () => {
|
|
1339
1616
|
clearTimer();
|
|
1340
1617
|
timerRef.current = setTimeout(() => {
|
|
@@ -1378,6 +1655,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1378
1655
|
saveNowRef.current = saveNow;
|
|
1379
1656
|
|
|
1380
1657
|
const commitSegments = (next, options = {}) => {
|
|
1658
|
+
invalidateBatchUndo();
|
|
1381
1659
|
segmentsRef.current = next;
|
|
1382
1660
|
setSegments(next);
|
|
1383
1661
|
pruneSelectionFor(next);
|
|
@@ -1396,11 +1674,20 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1396
1674
|
segmentsRef.current = [];
|
|
1397
1675
|
setSegments([]);
|
|
1398
1676
|
setSelection(new Set());
|
|
1677
|
+
setSegmentUndo(new Map());
|
|
1678
|
+
setBatchUndoState(null);
|
|
1679
|
+
setTitleEditing('');
|
|
1680
|
+
setPreviewImage(null);
|
|
1681
|
+
setDeleteConfirm('');
|
|
1682
|
+
pendingScrollRef.current = null;
|
|
1399
1683
|
setStatus('idle');
|
|
1400
1684
|
setError('');
|
|
1401
1685
|
return undefined;
|
|
1402
1686
|
}
|
|
1403
|
-
if (dirtyRef.current && rootRef.current !== ''
|
|
1687
|
+
if (dirtyRef.current && rootRef.current !== '') {
|
|
1688
|
+
// Flush the old workspace/session note before this binding replaces
|
|
1689
|
+
// the in-memory segments; a same-workspace session switch must not
|
|
1690
|
+
// drop a still-debounced edit.
|
|
1404
1691
|
saveNowRef.current?.({ force: true });
|
|
1405
1692
|
}
|
|
1406
1693
|
clearTimer();
|
|
@@ -1412,6 +1699,12 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1412
1699
|
segmentsRef.current = [];
|
|
1413
1700
|
setSegments([]);
|
|
1414
1701
|
setSelection(new Set());
|
|
1702
|
+
setSegmentUndo(new Map());
|
|
1703
|
+
setBatchUndoState(null);
|
|
1704
|
+
setTitleEditing('');
|
|
1705
|
+
setPreviewImage(null);
|
|
1706
|
+
setDeleteConfirm('');
|
|
1707
|
+
pendingScrollRef.current = null;
|
|
1415
1708
|
setStatus('loading');
|
|
1416
1709
|
setError('');
|
|
1417
1710
|
const load = () => {
|
|
@@ -1430,6 +1723,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1430
1723
|
segmentsRef.current = value;
|
|
1431
1724
|
setSegments(value);
|
|
1432
1725
|
setSelection(new Set());
|
|
1726
|
+
setSegmentUndo(new Map());
|
|
1727
|
+
setBatchUndoState(null);
|
|
1433
1728
|
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
1434
1729
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
1435
1730
|
dirtyRef.current = false;
|
|
@@ -1566,14 +1861,36 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1566
1861
|
autoGrow(element);
|
|
1567
1862
|
}, [segments]);
|
|
1568
1863
|
|
|
1864
|
+
// Restore a captured scroll offset right after a refresh commits ------
|
|
1865
|
+
useLayoutEffect(() => {
|
|
1866
|
+
const pending = pendingScrollRef.current;
|
|
1867
|
+
if (pending === null || pending === undefined) return;
|
|
1868
|
+
pendingScrollRef.current = null;
|
|
1869
|
+
const list = listRef.current;
|
|
1870
|
+
if (list === null || list === undefined) return;
|
|
1871
|
+
try {
|
|
1872
|
+
list.scrollTop = pending;
|
|
1873
|
+
} catch {
|
|
1874
|
+
/* A detached list cannot be scrolled. */
|
|
1875
|
+
}
|
|
1876
|
+
}, [segments]);
|
|
1877
|
+
|
|
1569
1878
|
// Keyboard ------------------------------------------------------------
|
|
1570
1879
|
useEffect(() => {
|
|
1571
1880
|
const onKeyDown = (event) => {
|
|
1572
1881
|
const chord = event.ctrlKey || event.metaKey;
|
|
1573
|
-
if (!chord || !event.shiftKey) return;
|
|
1574
|
-
if (event.key
|
|
1575
|
-
|
|
1576
|
-
|
|
1882
|
+
if (!chord || !event.shiftKey || event.altKey) return;
|
|
1883
|
+
if (event.key === 'N' || event.key === 'n') {
|
|
1884
|
+
event.preventDefault();
|
|
1885
|
+
setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
if (!resetShortcutHandledByHost
|
|
1889
|
+
&& (event.key === '0' || event.key === ')' || event.code === 'Digit0'
|
|
1890
|
+
|| event.code === 'Numpad0' || event.keyCode === 48)) {
|
|
1891
|
+
event.preventDefault();
|
|
1892
|
+
resetGeometry();
|
|
1893
|
+
}
|
|
1577
1894
|
};
|
|
1578
1895
|
window.addEventListener('keydown', onKeyDown, true);
|
|
1579
1896
|
return () => {
|
|
@@ -1585,6 +1902,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1585
1902
|
};
|
|
1586
1903
|
}, []);
|
|
1587
1904
|
|
|
1905
|
+
// Close the preview / delete confirmation with Escape ----------------
|
|
1906
|
+
useEffect(() => {
|
|
1907
|
+
if (previewImage === null && deleteConfirm === '') return undefined;
|
|
1908
|
+
const onKeyDown = (event) => {
|
|
1909
|
+
if (event.key !== 'Escape') return;
|
|
1910
|
+
event.preventDefault();
|
|
1911
|
+
setPreviewImage(null);
|
|
1912
|
+
setDeleteConfirm('');
|
|
1913
|
+
};
|
|
1914
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
1915
|
+
return () => window.removeEventListener('keydown', onKeyDown, true);
|
|
1916
|
+
}, [previewImage, deleteConfirm]);
|
|
1917
|
+
|
|
1588
1918
|
// Flush on page hide / reload -----------------------------------------
|
|
1589
1919
|
useEffect(() => {
|
|
1590
1920
|
const flush = () => {
|
|
@@ -1616,10 +1946,25 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1616
1946
|
const active = typeof document !== 'undefined' ? document.activeElement : null;
|
|
1617
1947
|
if (active !== null && active !== undefined && typeof active.closest === 'function'
|
|
1618
1948
|
&& active.closest('[data-dsh-notepad="true"]') !== null) return;
|
|
1949
|
+
// Snapshot the scroll offset right before the refresh; the list may
|
|
1950
|
+
// re-render (and jump) while the updated note is applied.
|
|
1951
|
+
const list = listRef.current;
|
|
1952
|
+
const scrollTop = list !== null && list !== undefined && Number.isFinite(list.scrollTop)
|
|
1953
|
+
? list.scrollTop
|
|
1954
|
+
: null;
|
|
1955
|
+
const mySeq = ++refreshSeqRef.current;
|
|
1956
|
+
pendingScrollRef.current = scrollTop;
|
|
1619
1957
|
fetchNote(root, sessionId)
|
|
1620
1958
|
.then((data) => {
|
|
1621
|
-
if (
|
|
1622
|
-
if (
|
|
1959
|
+
if (mySeq !== refreshSeqRef.current) return;
|
|
1960
|
+
if (rootRef.current !== root || dirtyRef.current) {
|
|
1961
|
+
pendingScrollRef.current = null;
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (typeof data.mtimeMs === 'number' && data.mtimeMs === versionRef.current) {
|
|
1965
|
+
pendingScrollRef.current = null;
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1623
1968
|
let incoming = normalizeServerSegments(data);
|
|
1624
1969
|
if (isLegacyPayload(data)) incoming = incoming.slice().reverse();
|
|
1625
1970
|
if (incoming.length === 0) incoming = [newSegment('')];
|
|
@@ -1627,11 +1972,15 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1627
1972
|
segmentsRef.current = value;
|
|
1628
1973
|
setSegments(value);
|
|
1629
1974
|
pruneSelectionFor(value);
|
|
1975
|
+
setSegmentUndo(new Map());
|
|
1976
|
+
setBatchUndoState(null);
|
|
1630
1977
|
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
1631
1978
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
1632
1979
|
setStatus('saved');
|
|
1633
1980
|
})
|
|
1634
|
-
.catch(() => {
|
|
1981
|
+
.catch(() => {
|
|
1982
|
+
if (mySeq === refreshSeqRef.current) pendingScrollRef.current = null;
|
|
1983
|
+
});
|
|
1635
1984
|
};
|
|
1636
1985
|
const onVisibility = () => {
|
|
1637
1986
|
if (typeof document !== 'undefined' && document.visibilityState === 'visible') refresh();
|
|
@@ -1650,9 +1999,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1650
1999
|
|
|
1651
2000
|
// Segment mutations ---------------------------------------------------
|
|
1652
2001
|
const updateSegmentText = (id, value) => {
|
|
2002
|
+
invalidateBatchUndo();
|
|
1653
2003
|
const next = segmentsRef.current.map((segment) => (segment.id === id ? { ...segment, text: value } : segment));
|
|
1654
2004
|
segmentsRef.current = next;
|
|
1655
2005
|
setSegments(next);
|
|
2006
|
+
setSegmentUndo((previous) => {
|
|
2007
|
+
const record = previous.get(id);
|
|
2008
|
+
if (record === undefined || record.after === value) return previous;
|
|
2009
|
+
const pruned = new Map(previous);
|
|
2010
|
+
pruned.delete(id);
|
|
2011
|
+
return pruned;
|
|
2012
|
+
});
|
|
1656
2013
|
dirtyRef.current = true;
|
|
1657
2014
|
setStatus('dirty');
|
|
1658
2015
|
setError('');
|
|
@@ -1663,6 +2020,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1663
2020
|
const title = String(value ?? '').slice(0, MAX_TITLE_CHARS);
|
|
1664
2021
|
const target = segmentsRef.current.find((segment) => segment.id === id);
|
|
1665
2022
|
if (target === undefined || target.title === title) return;
|
|
2023
|
+
invalidateBatchUndo();
|
|
1666
2024
|
const next = segmentsRef.current.map((segment) => (segment.id === id ? { ...segment, title } : segment));
|
|
1667
2025
|
segmentsRef.current = next;
|
|
1668
2026
|
setSegments(next);
|
|
@@ -1785,6 +2143,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1785
2143
|
const stillReferenced = next.some((segment) =>
|
|
1786
2144
|
(Array.isArray(segment.images) ? segment.images : []).some((image) => image.path === pathValue));
|
|
1787
2145
|
commitSegments(next);
|
|
2146
|
+
if (previewImage !== null && previewImage.path === pathValue) setPreviewImage(null);
|
|
1788
2147
|
if (!stillReferenced) deleteImageFile(pathValue);
|
|
1789
2148
|
};
|
|
1790
2149
|
|
|
@@ -1815,7 +2174,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1815
2174
|
const files = clipboardImageFiles(data);
|
|
1816
2175
|
if (files.length === 0) return;
|
|
1817
2176
|
event.preventDefault?.();
|
|
1818
|
-
event
|
|
2177
|
+
event?.stopPropagation?.();
|
|
1819
2178
|
attachImages(id, files, 'paste');
|
|
1820
2179
|
};
|
|
1821
2180
|
|
|
@@ -1824,7 +2183,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1824
2183
|
setDragSegment('');
|
|
1825
2184
|
if (files.length === 0) return;
|
|
1826
2185
|
event.preventDefault?.();
|
|
1827
|
-
event
|
|
2186
|
+
event?.stopPropagation?.();
|
|
1828
2187
|
attachImages(id, files, 'pick');
|
|
1829
2188
|
};
|
|
1830
2189
|
|
|
@@ -1844,11 +2203,15 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1844
2203
|
};
|
|
1845
2204
|
|
|
1846
2205
|
const openImage = (image) => {
|
|
2206
|
+
setPreviewImage({ path: image.path, name: image.name });
|
|
2207
|
+
};
|
|
2208
|
+
|
|
2209
|
+
const openOriginalImage = (image) => {
|
|
1847
2210
|
const url = imageUrl(image.path, rootRef.current);
|
|
1848
2211
|
try {
|
|
1849
2212
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
1850
2213
|
} catch {
|
|
1851
|
-
/* Popups may be blocked; the
|
|
2214
|
+
/* Popups may be blocked; the in-panel preview still works. */
|
|
1852
2215
|
}
|
|
1853
2216
|
};
|
|
1854
2217
|
|
|
@@ -1935,6 +2298,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1935
2298
|
if (record === null || record === undefined) return;
|
|
1936
2299
|
undoRef.current = null;
|
|
1937
2300
|
clearPendingImageDeletes();
|
|
2301
|
+
setSegmentUndo(new Map());
|
|
2302
|
+
setBatchUndoState(null);
|
|
1938
2303
|
commitSegments(record.snapshot);
|
|
1939
2304
|
setToast(null);
|
|
1940
2305
|
};
|
|
@@ -1949,6 +2314,28 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1949
2314
|
showToast(t('deletedToast'), { label: t('undo'), onClick: undoLast });
|
|
1950
2315
|
};
|
|
1951
2316
|
|
|
2317
|
+
/** Irreversible delete for an active segment, gated by a confirmation. */
|
|
2318
|
+
const deleteActiveSegment = (id) => {
|
|
2319
|
+
setDeleteConfirm('');
|
|
2320
|
+
setPreviewImage(null);
|
|
2321
|
+
const target = segmentsRef.current.find((segment) => segment.id === id && segment.archived !== true);
|
|
2322
|
+
if (target === undefined) return;
|
|
2323
|
+
// This delete cannot be undone: retire any older snackbar undo so it
|
|
2324
|
+
// cannot resurrect the segment through a stale snapshot.
|
|
2325
|
+
undoRef.current = null;
|
|
2326
|
+
setToast(null);
|
|
2327
|
+
setSegmentUndo((previous) => {
|
|
2328
|
+
if (!previous.has(id)) return previous;
|
|
2329
|
+
const updated = new Map(previous);
|
|
2330
|
+
updated.delete(id);
|
|
2331
|
+
return updated;
|
|
2332
|
+
});
|
|
2333
|
+
const next = segmentsRef.current.filter((segment) => segment.id !== id);
|
|
2334
|
+
commitSegments(next);
|
|
2335
|
+
scheduleImageCleanup(target.images);
|
|
2336
|
+
showToast(t('deletedToast'));
|
|
2337
|
+
};
|
|
2338
|
+
|
|
1952
2339
|
const clearArchive = () => {
|
|
1953
2340
|
const archived = segmentsRef.current.filter((segment) => segment.archived === true);
|
|
1954
2341
|
if (archived.length === 0) return;
|
|
@@ -2046,7 +2433,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2046
2433
|
const types = event?.dataTransfer?.types;
|
|
2047
2434
|
if (types !== undefined && types !== null && !Array.from(types).includes('application/x-dsh-notepad-segment')) return;
|
|
2048
2435
|
event.preventDefault?.();
|
|
2049
|
-
event
|
|
2436
|
+
event?.stopPropagation?.();
|
|
2050
2437
|
const rect = typeof event.currentTarget?.getBoundingClientRect === 'function'
|
|
2051
2438
|
? event.currentTarget.getBoundingClientRect()
|
|
2052
2439
|
: null;
|
|
@@ -2071,7 +2458,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2071
2458
|
setReorderTarget(null);
|
|
2072
2459
|
if (source === '') return;
|
|
2073
2460
|
event.preventDefault?.();
|
|
2074
|
-
event
|
|
2461
|
+
event?.stopPropagation?.();
|
|
2075
2462
|
reorderSegments(source, id, target?.after === true);
|
|
2076
2463
|
};
|
|
2077
2464
|
|
|
@@ -2084,14 +2471,33 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2084
2471
|
setOptimizing(next);
|
|
2085
2472
|
};
|
|
2086
2473
|
|
|
2474
|
+
/** Cache key: the prompt depends on the workspace, title and card text. */
|
|
2475
|
+
const optimizationCacheKey = (root, segment) =>
|
|
2476
|
+
`${root}\u0000${segment.title ?? ''}\u0000${segment.text}`;
|
|
2477
|
+
|
|
2478
|
+
const rememberOptimization = (key, value) => {
|
|
2479
|
+
const cache = optimizeCacheRef.current;
|
|
2480
|
+
cache.delete(key);
|
|
2481
|
+
cache.set(key, value);
|
|
2482
|
+
while (cache.size > OPTIMIZE_CACHE_LIMIT) {
|
|
2483
|
+
const oldest = cache.keys().next();
|
|
2484
|
+
if (oldest.done === true) break;
|
|
2485
|
+
cache.delete(oldest.value);
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
|
|
2087
2489
|
const requestOptimization = async (segment) => {
|
|
2088
2490
|
const root = rootRef.current;
|
|
2089
2491
|
const sessionId = rootSessionRef.current;
|
|
2090
2492
|
if (root === '' || sessionId === '') throw new Error(t('optimizeUnavailable'));
|
|
2091
2493
|
if (segment.text.length > MAX_OPTIMIZE_CHARS) throw new Error(t('optimizeTooLong'));
|
|
2494
|
+
const cacheKey = optimizationCacheKey(root, segment);
|
|
2495
|
+
const cached = optimizeCacheRef.current.get(cacheKey);
|
|
2496
|
+
if (typeof cached === 'string' && cached !== '') return cached;
|
|
2092
2497
|
const data = await optimizeNoteText(root, sessionId, segment.text, segment.title ?? '');
|
|
2093
2498
|
const value = typeof data.text === 'string' ? data.text.trim() : '';
|
|
2094
2499
|
if (value === '') throw new Error(t('optimizeFailed'));
|
|
2500
|
+
rememberOptimization(cacheKey, value);
|
|
2095
2501
|
return value;
|
|
2096
2502
|
};
|
|
2097
2503
|
|
|
@@ -2102,18 +2508,21 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2102
2508
|
showToast(t('optimizeEmpty'));
|
|
2103
2509
|
return;
|
|
2104
2510
|
}
|
|
2105
|
-
const snapshot = segmentsRef.current;
|
|
2106
2511
|
const original = target.text;
|
|
2107
2512
|
markOptimizing(id, true);
|
|
2108
2513
|
try {
|
|
2109
2514
|
const value = await requestOptimization(target);
|
|
2110
2515
|
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
2111
2516
|
if (current === undefined || current.text !== original || value === original) return;
|
|
2112
|
-
undoRef.current = { snapshot };
|
|
2113
2517
|
const next = segmentsRef.current.map((segment) =>
|
|
2114
2518
|
(segment.id === id ? { ...segment, text: value } : segment));
|
|
2115
2519
|
commitSegments(next);
|
|
2116
|
-
|
|
2520
|
+
setSegmentUndo((previous) => {
|
|
2521
|
+
const updated = new Map(previous);
|
|
2522
|
+
updated.set(id, { before: original, after: value });
|
|
2523
|
+
return updated;
|
|
2524
|
+
});
|
|
2525
|
+
showToast(t('optimizedToast'));
|
|
2117
2526
|
} catch (reason) {
|
|
2118
2527
|
const message = String(reason?.message ?? reason);
|
|
2119
2528
|
showToast(message === t('optimizeTooLong') || message === t('optimizeUnavailable')
|
|
@@ -2124,20 +2533,46 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2124
2533
|
}
|
|
2125
2534
|
};
|
|
2126
2535
|
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2536
|
+
const undoOptimizeSegment = (id) => {
|
|
2537
|
+
const record = segmentUndo.get(id);
|
|
2538
|
+
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
2539
|
+
if (record !== undefined && current !== undefined && current.text === record.after) {
|
|
2540
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2541
|
+
(segment.id === id ? { ...segment, text: record.before } : segment));
|
|
2542
|
+
commitSegments(next);
|
|
2543
|
+
setSegmentUndo((previous) => {
|
|
2544
|
+
if (!previous.has(id)) return previous;
|
|
2545
|
+
const updated = new Map(previous);
|
|
2546
|
+
updated.delete(id);
|
|
2547
|
+
return updated;
|
|
2548
|
+
});
|
|
2549
|
+
showToast(t('optimizeUndone'));
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
setSegmentUndo((previous) => {
|
|
2553
|
+
if (!previous.has(id)) return previous;
|
|
2554
|
+
const updated = new Map(previous);
|
|
2555
|
+
updated.delete(id);
|
|
2556
|
+
return updated;
|
|
2557
|
+
});
|
|
2558
|
+
};
|
|
2559
|
+
|
|
2560
|
+
/** Improve only the checked segments; identical text is answered from cache. */
|
|
2561
|
+
const optimizeSelectedSegments = async () => {
|
|
2562
|
+
if (optimizeBatchRef.current) return;
|
|
2563
|
+
const ids = new Set(selectedRef.current);
|
|
2129
2564
|
const targets = segmentsRef.current.filter((segment) =>
|
|
2130
|
-
segment.archived !== true && segment.text.trim() !== '');
|
|
2565
|
+
segment.archived !== true && ids.has(segment.id) && segment.text.trim() !== '');
|
|
2131
2566
|
if (targets.length === 0) {
|
|
2132
2567
|
showToast(t('optimizeEmpty'));
|
|
2133
2568
|
return;
|
|
2134
2569
|
}
|
|
2135
|
-
|
|
2570
|
+
optimizeBatchRef.current = true;
|
|
2136
2571
|
const snapshot = segmentsRef.current;
|
|
2137
2572
|
let done = 0;
|
|
2138
2573
|
let changed = 0;
|
|
2139
2574
|
let failed = 0;
|
|
2140
|
-
|
|
2575
|
+
setOptimizeBatch({ done, total: targets.length });
|
|
2141
2576
|
try {
|
|
2142
2577
|
for (const target of targets) {
|
|
2143
2578
|
const current = segmentsRef.current.find((segment) =>
|
|
@@ -2152,6 +2587,11 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2152
2587
|
? { ...segment, text: value }
|
|
2153
2588
|
: segment));
|
|
2154
2589
|
commitSegments(next);
|
|
2590
|
+
setSegmentUndo((previous) => {
|
|
2591
|
+
const updated = new Map(previous);
|
|
2592
|
+
updated.set(target.id, { before: current.text, after: value });
|
|
2593
|
+
return updated;
|
|
2594
|
+
});
|
|
2155
2595
|
changed += 1;
|
|
2156
2596
|
}
|
|
2157
2597
|
} catch {
|
|
@@ -2161,20 +2601,29 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2161
2601
|
}
|
|
2162
2602
|
}
|
|
2163
2603
|
done += 1;
|
|
2164
|
-
|
|
2604
|
+
setOptimizeBatch({ done, total: targets.length });
|
|
2165
2605
|
}
|
|
2166
2606
|
if (changed > 0) {
|
|
2167
|
-
|
|
2168
|
-
showToast(t('optimizedCount', { n: changed })
|
|
2607
|
+
setBatchUndoState({ snapshot });
|
|
2608
|
+
showToast(t('optimizedCount', { n: changed }));
|
|
2169
2609
|
} else {
|
|
2170
2610
|
showToast(failed > 0 ? t('optimizeFailed') : t('optimizedToast'));
|
|
2171
2611
|
}
|
|
2172
2612
|
} finally {
|
|
2173
|
-
|
|
2174
|
-
|
|
2613
|
+
optimizeBatchRef.current = false;
|
|
2614
|
+
setOptimizeBatch(null);
|
|
2175
2615
|
}
|
|
2176
2616
|
};
|
|
2177
2617
|
|
|
2618
|
+
const undoBatchOptimization = () => {
|
|
2619
|
+
const record = batchUndoRef.current;
|
|
2620
|
+
if (record === null || record === undefined) return;
|
|
2621
|
+
setBatchUndoState(null);
|
|
2622
|
+
setSegmentUndo(new Map());
|
|
2623
|
+
commitSegments(record.snapshot);
|
|
2624
|
+
showToast(t('optimizeUndone'));
|
|
2625
|
+
};
|
|
2626
|
+
|
|
2178
2627
|
// Selection -----------------------------------------------------------
|
|
2179
2628
|
const toggleSelected = (id) => {
|
|
2180
2629
|
const next = new Set(selectedRef.current);
|
|
@@ -2232,6 +2681,28 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2232
2681
|
showToast(copied ? t('insertUnavailable') : t('copyFailed'));
|
|
2233
2682
|
};
|
|
2234
2683
|
|
|
2684
|
+
/** Start a new session in this workspace with the text already in its composer. */
|
|
2685
|
+
const insertNewSession = async (list) => {
|
|
2686
|
+
const value = mergedText(list);
|
|
2687
|
+
if (value === '') {
|
|
2688
|
+
showToast(t('insertEmpty'));
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
const uiWorkspace = ctx.get('uiWorkspace');
|
|
2692
|
+
if (uiWorkspace !== undefined && uiWorkspace !== null && typeof uiWorkspace.startSession === 'function') {
|
|
2693
|
+
try {
|
|
2694
|
+
if (dirtyRef.current) await saveNowRef.current?.({ force: true });
|
|
2695
|
+
uiWorkspace.startSession(undefined, { prompt: value, clearPreviousDraft: true });
|
|
2696
|
+
showToast(t('insertNewDone'));
|
|
2697
|
+
return;
|
|
2698
|
+
} catch {
|
|
2699
|
+
/* Fall through to copy. */
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
const copied = await copyText(value);
|
|
2703
|
+
showToast(copied ? t('insertNewUnavailable') : t('copyFailed'));
|
|
2704
|
+
};
|
|
2705
|
+
|
|
2235
2706
|
const replaceSegments = async (list) => {
|
|
2236
2707
|
const value = mergedText(list);
|
|
2237
2708
|
if (value === '') {
|
|
@@ -2246,55 +2717,93 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2246
2717
|
showToast(copied ? t('replaceUnavailable') : t('copyFailed'));
|
|
2247
2718
|
};
|
|
2248
2719
|
|
|
2720
|
+
/** Resolve queue vs steer exactly like the composer's Send button. */
|
|
2721
|
+
const resolveSendMode = (sessionId) => {
|
|
2722
|
+
try {
|
|
2723
|
+
const sessions = ctx.get('sessions');
|
|
2724
|
+
const row = sessions?.list?.getSnapshot?.()?.byId?.[sessionId];
|
|
2725
|
+
if (row?.running !== true) return 'queue';
|
|
2726
|
+
} catch {
|
|
2727
|
+
return 'queue';
|
|
2728
|
+
}
|
|
2729
|
+
return busyEnterPreference(ctx);
|
|
2730
|
+
};
|
|
2731
|
+
|
|
2732
|
+
/** Send through the session face with an explicit queue/steer mode. */
|
|
2733
|
+
const promptSession = async (sessionId, value, mode) => {
|
|
2734
|
+
const sessions = ctx.get('sessions');
|
|
2735
|
+
if (sessions === undefined || sessions === null || typeof sessions.binding !== 'function') return false;
|
|
2736
|
+
const liveBinding = sessions.binding(sessionId);
|
|
2737
|
+
const session = liveBinding?.session;
|
|
2738
|
+
if (session === undefined || session === null || typeof session.prompt !== 'function') return false;
|
|
2739
|
+
let echo;
|
|
2740
|
+
try {
|
|
2741
|
+
echo = typeof session.beginSubmission === 'function'
|
|
2742
|
+
? session.beginSubmission({ mode, text: value, attachments: [] })
|
|
2743
|
+
: undefined;
|
|
2744
|
+
} catch {
|
|
2745
|
+
echo = undefined;
|
|
2746
|
+
}
|
|
2747
|
+
try {
|
|
2748
|
+
const result = await session.prompt([{ type: 'text', text: value }], mode, undefined, echo?.requestId);
|
|
2749
|
+
if (result !== undefined && result !== null && result.ok === false) {
|
|
2750
|
+
const failure = new Error(result.error?.message ?? 'prompt failed');
|
|
2751
|
+
failure.code = result.error?.code;
|
|
2752
|
+
throw failure;
|
|
2753
|
+
}
|
|
2754
|
+
return true;
|
|
2755
|
+
} catch (reason) {
|
|
2756
|
+
try {
|
|
2757
|
+
echo?.abandon?.();
|
|
2758
|
+
} catch {
|
|
2759
|
+
/* ignore */
|
|
2760
|
+
}
|
|
2761
|
+
throw reason;
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2249
2765
|
/**
|
|
2250
|
-
* Send text into
|
|
2251
|
-
*
|
|
2766
|
+
* Send text into a session. Queue prefers the documented scoped conversation
|
|
2767
|
+
* service; steer and its fallback go through the session face. The delivery
|
|
2768
|
+
* mode follows the General Settings "send while busy" preference.
|
|
2252
2769
|
* @returns {Promise<boolean>} whether a prompt was accepted
|
|
2253
2770
|
*/
|
|
2254
|
-
const sendToSession = async (value) => {
|
|
2255
|
-
const sessionId =
|
|
2771
|
+
const sendToSession = async (value, targetSessionId) => {
|
|
2772
|
+
const sessionId = typeof targetSessionId === 'string' && targetSessionId !== ''
|
|
2773
|
+
? targetSessionId
|
|
2774
|
+
: (rootSessionRef.current !== '' ? rootSessionRef.current : bindingRef.current?.id);
|
|
2256
2775
|
if (typeof sessionId !== 'string' || sessionId === '') return false;
|
|
2257
2776
|
const sessions = ctx.get('sessions');
|
|
2258
2777
|
if (sessions === undefined || sessions === null) return false;
|
|
2778
|
+
const mode = resolveSendMode(sessionId);
|
|
2779
|
+
let scoped;
|
|
2259
2780
|
if (typeof sessions.scope === 'function') {
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
if (conversation !== undefined && conversation !== null && typeof conversation.send === 'function') {
|
|
2265
|
-
await conversation.send(value);
|
|
2266
|
-
return true;
|
|
2781
|
+
try {
|
|
2782
|
+
scoped = sessions.scope(sessionId);
|
|
2783
|
+
} catch {
|
|
2784
|
+
scoped = undefined;
|
|
2267
2785
|
}
|
|
2268
2786
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
if (result !== undefined && result !== null && result.ok === false) {
|
|
2284
|
-
throw new Error(result.error?.message ?? 'prompt failed');
|
|
2285
|
-
}
|
|
2787
|
+
const conversation = scoped === undefined || scoped === null
|
|
2788
|
+
? undefined
|
|
2789
|
+
: typeof scoped.get === 'function' ? scoped.get('conversation') : scoped.conversation;
|
|
2790
|
+
if (mode === 'queue' && conversation !== undefined && conversation !== null
|
|
2791
|
+
&& typeof conversation.send === 'function') {
|
|
2792
|
+
await conversation.send(value);
|
|
2793
|
+
return true;
|
|
2794
|
+
}
|
|
2795
|
+
try {
|
|
2796
|
+
return await promptSession(sessionId, value, mode);
|
|
2797
|
+
} catch (reason) {
|
|
2798
|
+
if (mode === 'steer' && reason?.code === 'session/steer-unavailable') {
|
|
2799
|
+
if (conversation !== undefined && conversation !== null && typeof conversation.send === 'function') {
|
|
2800
|
+
await conversation.send(value);
|
|
2286
2801
|
return true;
|
|
2287
|
-
} catch (reason) {
|
|
2288
|
-
try {
|
|
2289
|
-
echo?.abandon?.();
|
|
2290
|
-
} catch {
|
|
2291
|
-
/* ignore */
|
|
2292
|
-
}
|
|
2293
|
-
throw reason;
|
|
2294
2802
|
}
|
|
2803
|
+
return promptSession(sessionId, value, 'queue');
|
|
2295
2804
|
}
|
|
2805
|
+
throw reason;
|
|
2296
2806
|
}
|
|
2297
|
-
return false;
|
|
2298
2807
|
};
|
|
2299
2808
|
|
|
2300
2809
|
const sendSegments = async (list) => {
|
|
@@ -2335,8 +2844,78 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2335
2844
|
}
|
|
2336
2845
|
};
|
|
2337
2846
|
|
|
2847
|
+
/** Create an empty session in a workspace and return its id. */
|
|
2848
|
+
const createWorkspaceSession = async (cwd) => {
|
|
2849
|
+
const sessions = ctx.get('sessions');
|
|
2850
|
+
if (sessions === undefined || sessions === null || typeof sessions.create !== 'function') return null;
|
|
2851
|
+
const result = await sessions.create({ cwd });
|
|
2852
|
+
if (result === undefined || result === null) return null;
|
|
2853
|
+
if (result.ok === false) {
|
|
2854
|
+
const failure = new Error(result.error?.message ?? 'create session failed');
|
|
2855
|
+
failure.code = result.error?.code;
|
|
2856
|
+
throw failure;
|
|
2857
|
+
}
|
|
2858
|
+
const id = result.value?.sessionId ?? result.sessionId
|
|
2859
|
+
?? (typeof result === 'string' ? result : undefined);
|
|
2860
|
+
return typeof id === 'string' && id !== '' ? id : null;
|
|
2861
|
+
};
|
|
2862
|
+
|
|
2863
|
+
/** Start a fresh session, send the text there, then switch to it. */
|
|
2864
|
+
const sendNewSession = async (list) => {
|
|
2865
|
+
if (sendingRef.current) return;
|
|
2866
|
+
const value = mergedText(list);
|
|
2867
|
+
if (value === '') {
|
|
2868
|
+
showToast(t('insertEmpty'));
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
const cwd = bindingRef.current?.cwd ?? rootRef.current;
|
|
2872
|
+
if (cwd === '') {
|
|
2873
|
+
showToast(t('sendNewUnavailable'));
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
sendingRef.current = true;
|
|
2877
|
+
setSending(true);
|
|
2878
|
+
try {
|
|
2879
|
+
const newSessionId = await createWorkspaceSession(cwd);
|
|
2880
|
+
if (newSessionId === null) {
|
|
2881
|
+
if (!insertValue(value)) await copyText(value);
|
|
2882
|
+
showToast(t('sendNewUnavailable'));
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const accepted = await sendToSession(value, newSessionId);
|
|
2886
|
+
if (accepted !== true) {
|
|
2887
|
+
if (!insertValue(value)) await copyText(value);
|
|
2888
|
+
showToast(t('sendUnavailable'));
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
showToast(t('sendNewDone'));
|
|
2892
|
+
const ids = new Set(
|
|
2893
|
+
list.filter((segment) => segment.archived !== true).map((segment) => segment.id),
|
|
2894
|
+
);
|
|
2895
|
+
if (ids.size > 0 && (rootRef.current === '' || rootRef.current === cwd)) {
|
|
2896
|
+
const archivedAt = new Date().toISOString();
|
|
2897
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2898
|
+
(ids.has(segment.id) ? { ...segment, archived: true, archivedAt } : segment));
|
|
2899
|
+
setSelection(new Set([...selectedRef.current].filter((id) => !ids.has(id))));
|
|
2900
|
+
commitSegments(next);
|
|
2901
|
+
await saveNowRef.current?.({ force: true });
|
|
2902
|
+
}
|
|
2903
|
+
const uiWorkspace = ctx.get('uiWorkspace');
|
|
2904
|
+
if (uiWorkspace !== undefined && uiWorkspace !== null && typeof uiWorkspace.openSession === 'function') {
|
|
2905
|
+
uiWorkspace.openSession(newSessionId);
|
|
2906
|
+
}
|
|
2907
|
+
} catch (reason) {
|
|
2908
|
+
showToast(`${t('sendFailed')}: ${String(reason?.message ?? reason)}`);
|
|
2909
|
+
} finally {
|
|
2910
|
+
sendingRef.current = false;
|
|
2911
|
+
setSending(false);
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
|
|
2338
2915
|
const copyAll = async () => {
|
|
2339
|
-
|
|
2916
|
+
// Copy only active work: archived segments were already sent or filed
|
|
2917
|
+
// away and must not leak back into a copy-all.
|
|
2918
|
+
const value = mergedText(segmentsRef.current.filter((segment) => segment.archived !== true));
|
|
2340
2919
|
if (value === '') {
|
|
2341
2920
|
showToast(t('insertEmpty'));
|
|
2342
2921
|
return;
|
|
@@ -2370,6 +2949,34 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2370
2949
|
updateSegmentText(id, event.target?.value ?? '');
|
|
2371
2950
|
};
|
|
2372
2951
|
|
|
2952
|
+
/** Focus a card's textarea and place the caret at an absolute offset. */
|
|
2953
|
+
const focusSegmentAt = (id, caret) => {
|
|
2954
|
+
const element = textareaRefs.current.get(id);
|
|
2955
|
+
if (element === null || element === undefined) return false;
|
|
2956
|
+
try {
|
|
2957
|
+
element.focus?.({ preventScroll: true });
|
|
2958
|
+
} catch {
|
|
2959
|
+
try {
|
|
2960
|
+
element.focus?.();
|
|
2961
|
+
} catch {
|
|
2962
|
+
/* ignore */
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
try {
|
|
2966
|
+
const value = typeof element.value === 'string' ? element.value : '';
|
|
2967
|
+
const position = Math.max(0, Math.min(Number(caret) || 0, value.length));
|
|
2968
|
+
if (typeof element.setSelectionRange === 'function') element.setSelectionRange(position, position);
|
|
2969
|
+
} catch {
|
|
2970
|
+
/* A non-layout shim may not support ranges. */
|
|
2971
|
+
}
|
|
2972
|
+
try {
|
|
2973
|
+
element.scrollIntoView?.({ block: 'nearest' });
|
|
2974
|
+
} catch {
|
|
2975
|
+
/* ignore */
|
|
2976
|
+
}
|
|
2977
|
+
return true;
|
|
2978
|
+
};
|
|
2979
|
+
|
|
2373
2980
|
const onSegmentKeyDown = (event, id) => {
|
|
2374
2981
|
if (event.isComposing === true || event.nativeEvent?.isComposing === true) return;
|
|
2375
2982
|
const chord = event.ctrlKey || event.metaKey;
|
|
@@ -2384,6 +2991,28 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2384
2991
|
insertSegments(list);
|
|
2385
2992
|
return;
|
|
2386
2993
|
}
|
|
2994
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
2995
|
+
if (event.altKey || event.shiftKey || chord) return;
|
|
2996
|
+
const element = event.currentTarget ?? event.target;
|
|
2997
|
+
const value = typeof element?.value === 'string' ? element.value : '';
|
|
2998
|
+
const start = typeof element?.selectionStart === 'number' ? element.selectionStart : null;
|
|
2999
|
+
const end = typeof element?.selectionEnd === 'number' ? element.selectionEnd : start;
|
|
3000
|
+
if (start === null || start !== end) return;
|
|
3001
|
+
const firstBreak = value.indexOf('\n');
|
|
3002
|
+
const lastBreak = value.lastIndexOf('\n');
|
|
3003
|
+
const up = event.key === 'ArrowUp';
|
|
3004
|
+
const atEdge = up
|
|
3005
|
+
? (firstBreak === -1 || start <= firstBreak)
|
|
3006
|
+
: (lastBreak === -1 || start > lastBreak);
|
|
3007
|
+
if (!atEdge) return;
|
|
3008
|
+
const active = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
3009
|
+
const position = active.findIndex((segment) => segment.id === id);
|
|
3010
|
+
const neighbor = position < 0 ? undefined : active[position + (up ? -1 : 1)];
|
|
3011
|
+
if (neighbor !== undefined && focusSegmentAt(neighbor.id, up ? neighbor.text.length : 0)) {
|
|
3012
|
+
event.preventDefault?.();
|
|
3013
|
+
}
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
2387
3016
|
if (event.key !== 'Enter' || event.shiftKey || event.altKey || chord) return;
|
|
2388
3017
|
event.preventDefault();
|
|
2389
3018
|
splitSegment(id, event.currentTarget ?? event.target);
|
|
@@ -2400,8 +3029,11 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2400
3029
|
const vw = window.innerWidth || 1280;
|
|
2401
3030
|
const vh = window.innerHeight || 800;
|
|
2402
3031
|
const height = ui.minimized ? HEAD_H : ui.height;
|
|
2403
|
-
const
|
|
2404
|
-
const
|
|
3032
|
+
const minY = hostChromeTop() + EDGE_GAP;
|
|
3033
|
+
const x = Math.min(Math.max(EDGE_GAP, start.x + moveEvent.clientX - start.px),
|
|
3034
|
+
Math.max(EDGE_GAP, vw - ui.width - EDGE_GAP));
|
|
3035
|
+
const y = Math.min(Math.max(minY, start.y + moveEvent.clientY - start.py),
|
|
3036
|
+
Math.max(minY, vh - height - EDGE_GAP));
|
|
2405
3037
|
setUi((previous) => ({ ...previous, x, y }));
|
|
2406
3038
|
};
|
|
2407
3039
|
const stop = () => {
|
|
@@ -2420,24 +3052,27 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2420
3052
|
if (ui.minimized) return;
|
|
2421
3053
|
if (event.button !== undefined && event.button !== 0) return;
|
|
2422
3054
|
event.preventDefault?.();
|
|
2423
|
-
event
|
|
3055
|
+
event?.stopPropagation?.();
|
|
2424
3056
|
const start = { px: event.clientX, py: event.clientY, width: ui.width, height: ui.height };
|
|
2425
3057
|
resizeRef.current = start;
|
|
2426
3058
|
setResizing(true);
|
|
2427
3059
|
const move = (moveEvent) => {
|
|
2428
3060
|
const vw = window.innerWidth || 1280;
|
|
2429
3061
|
const vh = window.innerHeight || 800;
|
|
2430
|
-
const
|
|
2431
|
-
const
|
|
3062
|
+
const minY = hostChromeTop() + EDGE_GAP;
|
|
3063
|
+
const width = Math.min(Math.max(MIN_W, start.width + moveEvent.clientX - start.px),
|
|
3064
|
+
Math.max(MIN_W, vw - EDGE_GAP * 2));
|
|
3065
|
+
const height = Math.min(Math.max(MIN_H, start.height + moveEvent.clientY - start.py),
|
|
3066
|
+
Math.max(MIN_H, vh - minY - EDGE_GAP));
|
|
2432
3067
|
setUi((previous) => {
|
|
2433
|
-
const maxX = Math.max(
|
|
2434
|
-
const maxY = Math.max(
|
|
3068
|
+
const maxX = Math.max(EDGE_GAP, vw - width - EDGE_GAP);
|
|
3069
|
+
const maxY = Math.max(minY, vh - height - EDGE_GAP);
|
|
2435
3070
|
return {
|
|
2436
3071
|
...previous,
|
|
2437
3072
|
width,
|
|
2438
3073
|
height,
|
|
2439
|
-
x: typeof previous.x === 'number' ? Math.min(previous.x, maxX) : previous.x,
|
|
2440
|
-
y: typeof previous.y === 'number' ? Math.min(previous.y, maxY) : previous.y,
|
|
3074
|
+
x: typeof previous.x === 'number' ? Math.min(Math.max(EDGE_GAP, previous.x), maxX) : previous.x,
|
|
3075
|
+
y: typeof previous.y === 'number' ? Math.min(Math.max(minY, previous.y), maxY) : previous.y,
|
|
2441
3076
|
};
|
|
2442
3077
|
});
|
|
2443
3078
|
};
|
|
@@ -2463,10 +3098,12 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2463
3098
|
const allSelected = activeSegments.length > 0 && selectedCount === activeSegments.length;
|
|
2464
3099
|
const unsentCount = countUnsent(segments);
|
|
2465
3100
|
const totalChars = segments.reduce((sum, segment) => sum + segment.text.length, 0);
|
|
3101
|
+
const activeChars = activeSegments.reduce((sum, segment) => sum + segment.text.length, 0);
|
|
2466
3102
|
const displayWidth = ui.width;
|
|
2467
3103
|
const displayHeight = ui.minimized ? HEAD_H : ui.height;
|
|
2468
|
-
const fallbackX = Math.max(
|
|
2469
|
-
const fallbackY = Math.max(
|
|
3104
|
+
const fallbackX = Math.max(EDGE_GAP, ((typeof window !== 'undefined' && window.innerWidth) || 1280) - displayWidth - 24);
|
|
3105
|
+
const fallbackY = Math.max(hostChromeTop() + EDGE_GAP,
|
|
3106
|
+
((typeof window !== 'undefined' && window.innerHeight) || 800) - displayHeight - 24);
|
|
2470
3107
|
const left = typeof ui.x === 'number' ? ui.x : fallbackX;
|
|
2471
3108
|
const top = typeof ui.y === 'number' ? ui.y : fallbackY;
|
|
2472
3109
|
|
|
@@ -2489,6 +3126,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2489
3126
|
h('span', { className: 'dsh-np-title' }, t('title')),
|
|
2490
3127
|
h('span', { className: 'dsh-np-workspace', title: binding.cwd }, workspaceName(binding.cwd)),
|
|
2491
3128
|
h('span', { className: 'dsh-np-spacer' }),
|
|
3129
|
+
h(
|
|
3130
|
+
'button',
|
|
3131
|
+
{
|
|
3132
|
+
type: 'button',
|
|
3133
|
+
className: 'dsh-np-btn',
|
|
3134
|
+
title: `${t('resetShortcut')} · Ctrl+Shift+0`,
|
|
3135
|
+
'aria-label': t('resetShortcut'),
|
|
3136
|
+
onClick: resetGeometry,
|
|
3137
|
+
},
|
|
3138
|
+
h(ResetIcon, { size: 15 }),
|
|
3139
|
+
),
|
|
2492
3140
|
h(
|
|
2493
3141
|
'button',
|
|
2494
3142
|
{
|
|
@@ -2496,7 +3144,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2496
3144
|
className: 'dsh-np-btn',
|
|
2497
3145
|
title: t('copy'),
|
|
2498
3146
|
'aria-label': t('copy'),
|
|
2499
|
-
disabled:
|
|
3147
|
+
disabled: activeChars === 0,
|
|
2500
3148
|
onClick: copyAll,
|
|
2501
3149
|
},
|
|
2502
3150
|
h(CopyIcon, { size: 15 }),
|
|
@@ -2549,6 +3197,37 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2549
3197
|
),
|
|
2550
3198
|
);
|
|
2551
3199
|
|
|
3200
|
+
/** One icon that expands into current-session + new-session actions. */
|
|
3201
|
+
const actionGroup = ({ size, primary, extra, disabled = false }) => {
|
|
3202
|
+
const base = size === 'bar' ? 'dsh-np-btn' : 'dsh-np-segbtn';
|
|
3203
|
+
const primaryProps = {
|
|
3204
|
+
type: 'button',
|
|
3205
|
+
className: base,
|
|
3206
|
+
title: primary.title,
|
|
3207
|
+
'aria-label': primary.title,
|
|
3208
|
+
disabled,
|
|
3209
|
+
onClick: primary.onClick,
|
|
3210
|
+
};
|
|
3211
|
+
if (primary.accent === true) primaryProps['data-accent'] = 'true';
|
|
3212
|
+
return h(
|
|
3213
|
+
'div',
|
|
3214
|
+
{ className: 'dsh-np-actiongroup', 'data-size': size },
|
|
3215
|
+
h('button', primaryProps, primary.icon),
|
|
3216
|
+
h(
|
|
3217
|
+
'button',
|
|
3218
|
+
{
|
|
3219
|
+
type: 'button',
|
|
3220
|
+
className: `${base} dsh-np-group-extra`,
|
|
3221
|
+
title: extra.title,
|
|
3222
|
+
'aria-label': extra.title,
|
|
3223
|
+
disabled,
|
|
3224
|
+
onClick: extra.onClick,
|
|
3225
|
+
},
|
|
3226
|
+
h(NewSessionIcon, { size: 14 }),
|
|
3227
|
+
),
|
|
3228
|
+
);
|
|
3229
|
+
};
|
|
3230
|
+
|
|
2552
3231
|
const tools = h(
|
|
2553
3232
|
'div',
|
|
2554
3233
|
{ className: 'dsh-np-tools' },
|
|
@@ -2566,18 +3245,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2566
3245
|
selectedCount > 0 ? t('selected', { n: selectedCount }) : t('selectHint')),
|
|
2567
3246
|
),
|
|
2568
3247
|
h('span', { className: 'dsh-np-spacer' }),
|
|
2569
|
-
|
|
2570
|
-
'
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
className: 'dsh-np-btn',
|
|
3248
|
+
actionGroup({
|
|
3249
|
+
size: 'bar',
|
|
3250
|
+
disabled: selectedCount === 0,
|
|
3251
|
+
primary: {
|
|
2574
3252
|
title: selectedCount > 0 ? t('insertSelected', { n: selectedCount }) : t('insert'),
|
|
2575
|
-
'aria-label': t('insert'),
|
|
2576
|
-
disabled: selectedCount === 0,
|
|
2577
3253
|
onClick: () => insertSegments(selectedActive),
|
|
3254
|
+
icon: h(InsertIcon, { size: 14 }),
|
|
2578
3255
|
},
|
|
2579
|
-
|
|
2580
|
-
|
|
3256
|
+
extra: {
|
|
3257
|
+
title: selectedCount > 0 ? t('insertNewSelected', { n: selectedCount }) : t('insertNewSession'),
|
|
3258
|
+
onClick: () => insertNewSession(selectedActive),
|
|
3259
|
+
},
|
|
3260
|
+
}),
|
|
2581
3261
|
h(
|
|
2582
3262
|
'button',
|
|
2583
3263
|
{
|
|
@@ -2590,21 +3270,22 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2590
3270
|
},
|
|
2591
3271
|
h(ReplaceIcon, { size: 14 }),
|
|
2592
3272
|
),
|
|
2593
|
-
|
|
2594
|
-
'
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
className: 'dsh-np-btn',
|
|
2598
|
-
'data-accent': 'true',
|
|
3273
|
+
actionGroup({
|
|
3274
|
+
size: 'bar',
|
|
3275
|
+
disabled: selectedCount === 0 || sending,
|
|
3276
|
+
primary: {
|
|
2599
3277
|
title: selectedCount > 0
|
|
2600
3278
|
? (sending ? t('sending') : t('sendSelected', { n: selectedCount }))
|
|
2601
3279
|
: t('send'),
|
|
2602
|
-
'aria-label': t('send'),
|
|
2603
|
-
disabled: selectedCount === 0 || sending,
|
|
2604
3280
|
onClick: () => sendSegments(selectedActive),
|
|
3281
|
+
icon: h(SendIcon, { size: 14 }),
|
|
3282
|
+
accent: true,
|
|
2605
3283
|
},
|
|
2606
|
-
|
|
2607
|
-
|
|
3284
|
+
extra: {
|
|
3285
|
+
title: selectedCount > 0 ? t('sendNewSelected', { n: selectedCount }) : t('sendNewSession'),
|
|
3286
|
+
onClick: () => sendNewSession(selectedActive),
|
|
3287
|
+
},
|
|
3288
|
+
}),
|
|
2608
3289
|
h(
|
|
2609
3290
|
'button',
|
|
2610
3291
|
{
|
|
@@ -2622,17 +3303,24 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2622
3303
|
{
|
|
2623
3304
|
type: 'button',
|
|
2624
3305
|
className: 'dsh-np-btn',
|
|
2625
|
-
'data-active':
|
|
2626
|
-
title:
|
|
2627
|
-
? t('optimizeAllProgress', { done:
|
|
2628
|
-
:
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
3306
|
+
'data-active': optimizeBatch !== null || batchUndo !== null ? 'true' : 'false',
|
|
3307
|
+
title: optimizeBatch !== null
|
|
3308
|
+
? t('optimizeAllProgress', { done: optimizeBatch.done, total: optimizeBatch.total })
|
|
3309
|
+
: batchUndo !== null
|
|
3310
|
+
? t('undoOptimize')
|
|
3311
|
+
: selectedCount > 0
|
|
3312
|
+
? t('optimizeSelected', { n: selectedCount })
|
|
3313
|
+
: t('optimizeSelectHint'),
|
|
3314
|
+
'aria-label': batchUndo !== null
|
|
3315
|
+
? t('undoOptimize')
|
|
3316
|
+
: selectedCount > 0 ? t('optimizeSelected', { n: selectedCount }) : t('optimizeSelectHint'),
|
|
3317
|
+
disabled: optimizeBatch !== null
|
|
3318
|
+
|| (batchUndo === null && selectedCount === 0),
|
|
3319
|
+
onClick: batchUndo !== null ? undoBatchOptimization : optimizeSelectedSegments,
|
|
2632
3320
|
},
|
|
2633
|
-
|
|
3321
|
+
optimizeBatch !== null
|
|
2634
3322
|
? h('span', { className: 'dsh-np-mini-spinner' })
|
|
2635
|
-
: h(SparkleIcon, { size: 14 }),
|
|
3323
|
+
: batchUndo !== null ? h(UndoIcon, { size: 14 }) : h(SparkleIcon, { size: 14 }),
|
|
2636
3324
|
),
|
|
2637
3325
|
h(
|
|
2638
3326
|
'button',
|
|
@@ -2720,6 +3408,15 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2720
3408
|
const activeCards = activeSegments.map((segment, index) => {
|
|
2721
3409
|
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
2722
3410
|
const empty = segment.text.trim() === '' && images.length === 0;
|
|
3411
|
+
const titleValue = segment.title ?? '';
|
|
3412
|
+
const showTitleInput = titleValue !== '' || titleEditing === segment.id;
|
|
3413
|
+
const undoRecord = segmentUndo.get(segment.id);
|
|
3414
|
+
const canUndoOptimize = undoRecord !== undefined && undoRecord.after === segment.text;
|
|
3415
|
+
const optimizingNow = optimizing.has(segment.id);
|
|
3416
|
+
const aiBusy = optimizingNow || optimizeBatch !== null;
|
|
3417
|
+
const aiTitle = aiBusy
|
|
3418
|
+
? t('optimizing')
|
|
3419
|
+
: canUndoOptimize ? t('undoOptimize') : t('optimizeSegment');
|
|
2723
3420
|
return h(
|
|
2724
3421
|
'article',
|
|
2725
3422
|
{
|
|
@@ -2786,28 +3483,42 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2786
3483
|
},
|
|
2787
3484
|
String(index + 1).padStart(2, '0'),
|
|
2788
3485
|
),
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
3486
|
+
showTitleInput
|
|
3487
|
+
? h('input', {
|
|
3488
|
+
type: 'text',
|
|
3489
|
+
className: 'dsh-np-title-input',
|
|
3490
|
+
value: titleValue,
|
|
3491
|
+
placeholder: t('titlePlaceholder'),
|
|
3492
|
+
maxLength: MAX_TITLE_CHARS,
|
|
3493
|
+
autoFocus: titleValue === '',
|
|
3494
|
+
'aria-label': t('segmentTitle'),
|
|
3495
|
+
spellCheck: false,
|
|
3496
|
+
onFocus: () => setTitleEditing(segment.id),
|
|
3497
|
+
onChange: (event) => updateSegmentTitle(segment.id, event.target?.value ?? ''),
|
|
3498
|
+
onKeyDown: (event) => {
|
|
3499
|
+
if (event.key !== 'Enter') return;
|
|
3500
|
+
event.preventDefault();
|
|
3501
|
+
try {
|
|
3502
|
+
event.currentTarget?.blur?.();
|
|
3503
|
+
} catch {
|
|
3504
|
+
/* ignore */
|
|
3505
|
+
}
|
|
3506
|
+
},
|
|
3507
|
+
onBlur: () => {
|
|
3508
|
+
if (dirtyRef.current) saveNowRef.current?.();
|
|
3509
|
+
if ((segment.title ?? '') === '') setTitleEditing('');
|
|
3510
|
+
},
|
|
3511
|
+
})
|
|
3512
|
+
: h(
|
|
3513
|
+
'button',
|
|
3514
|
+
{
|
|
3515
|
+
type: 'button',
|
|
3516
|
+
className: 'dsh-np-title-add',
|
|
3517
|
+
title: t('segmentTitle'),
|
|
3518
|
+
'aria-label': t('segmentTitle'),
|
|
3519
|
+
onClick: () => setTitleEditing(segment.id),
|
|
3520
|
+
},
|
|
3521
|
+
),
|
|
2811
3522
|
h(
|
|
2812
3523
|
'button',
|
|
2813
3524
|
{
|
|
@@ -2819,18 +3530,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2819
3530
|
},
|
|
2820
3531
|
h(ImageIcon, { size: 14 }),
|
|
2821
3532
|
),
|
|
2822
|
-
|
|
2823
|
-
'
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
className: 'dsh-np-segbtn',
|
|
3533
|
+
actionGroup({
|
|
3534
|
+
size: 'card',
|
|
3535
|
+
disabled: empty,
|
|
3536
|
+
primary: {
|
|
2827
3537
|
title: t('insertSegment'),
|
|
2828
|
-
'aria-label': t('insertSegment'),
|
|
2829
|
-
disabled: empty,
|
|
2830
3538
|
onClick: () => insertSegments([segment]),
|
|
3539
|
+
icon: h(InsertIcon, { size: 14 }),
|
|
2831
3540
|
},
|
|
2832
|
-
|
|
2833
|
-
|
|
3541
|
+
extra: {
|
|
3542
|
+
title: t('insertNewSession'),
|
|
3543
|
+
onClick: () => insertNewSession([segment]),
|
|
3544
|
+
},
|
|
3545
|
+
}),
|
|
2834
3546
|
h(
|
|
2835
3547
|
'button',
|
|
2836
3548
|
{
|
|
@@ -2843,32 +3555,35 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2843
3555
|
},
|
|
2844
3556
|
h(ReplaceIcon, { size: 14 }),
|
|
2845
3557
|
),
|
|
2846
|
-
|
|
2847
|
-
'
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
className: 'dsh-np-segbtn',
|
|
3558
|
+
actionGroup({
|
|
3559
|
+
size: 'card',
|
|
3560
|
+
disabled: empty || sending,
|
|
3561
|
+
primary: {
|
|
2851
3562
|
title: t('sendSegment'),
|
|
2852
|
-
'aria-label': t('sendSegment'),
|
|
2853
|
-
disabled: empty || sending,
|
|
2854
3563
|
onClick: () => sendSegments([segment]),
|
|
3564
|
+
icon: h(SendIcon, { size: 14 }),
|
|
2855
3565
|
},
|
|
2856
|
-
|
|
2857
|
-
|
|
3566
|
+
extra: {
|
|
3567
|
+
title: t('sendNewSession'),
|
|
3568
|
+
onClick: () => sendNewSession([segment]),
|
|
3569
|
+
},
|
|
3570
|
+
}),
|
|
2858
3571
|
h(
|
|
2859
3572
|
'button',
|
|
2860
3573
|
{
|
|
2861
3574
|
type: 'button',
|
|
2862
3575
|
className: 'dsh-np-segbtn',
|
|
2863
3576
|
'data-ai': 'true',
|
|
2864
|
-
title:
|
|
2865
|
-
'aria-label':
|
|
2866
|
-
disabled:
|
|
2867
|
-
onClick:
|
|
3577
|
+
title: aiTitle,
|
|
3578
|
+
'aria-label': aiTitle,
|
|
3579
|
+
disabled: aiBusy || (canUndoOptimize ? false : empty),
|
|
3580
|
+
onClick: canUndoOptimize
|
|
3581
|
+
? () => undoOptimizeSegment(segment.id)
|
|
3582
|
+
: () => optimizeSegment(segment.id),
|
|
2868
3583
|
},
|
|
2869
|
-
|
|
3584
|
+
optimizingNow
|
|
2870
3585
|
? h('span', { className: 'dsh-np-mini-spinner' })
|
|
2871
|
-
: h(SparkleIcon, { size: 14 }),
|
|
3586
|
+
: canUndoOptimize ? h(UndoIcon, { size: 14 }) : h(SparkleIcon, { size: 14 }),
|
|
2872
3587
|
),
|
|
2873
3588
|
h(
|
|
2874
3589
|
'button',
|
|
@@ -2881,6 +3596,18 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2881
3596
|
},
|
|
2882
3597
|
h(ArchiveIcon, { size: 14 }),
|
|
2883
3598
|
),
|
|
3599
|
+
h(
|
|
3600
|
+
'button',
|
|
3601
|
+
{
|
|
3602
|
+
type: 'button',
|
|
3603
|
+
className: 'dsh-np-segbtn',
|
|
3604
|
+
'data-danger': 'true',
|
|
3605
|
+
title: t('deleteActiveSegment'),
|
|
3606
|
+
'aria-label': t('deleteActiveSegment'),
|
|
3607
|
+
onClick: () => setDeleteConfirm(segment.id),
|
|
3608
|
+
},
|
|
3609
|
+
h(TrashIcon, { size: 14 }),
|
|
3610
|
+
),
|
|
2884
3611
|
),
|
|
2885
3612
|
h(
|
|
2886
3613
|
'div',
|
|
@@ -3077,7 +3804,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
3077
3804
|
h('span', { className: 'dsh-np-count' },
|
|
3078
3805
|
`${t('chars', { n: totalChars })} · ${t('segments', { n: activeSegments.length })}`
|
|
3079
3806
|
+ (totalImages > 0 ? ` · ${t('images', { n: totalImages })}` : '')),
|
|
3080
|
-
h(
|
|
3807
|
+
h(
|
|
3808
|
+
'button',
|
|
3809
|
+
{
|
|
3810
|
+
type: 'button',
|
|
3811
|
+
className: 'dsh-np-hint',
|
|
3812
|
+
title: t('hintNext'),
|
|
3813
|
+
'aria-label': t('hintNext'),
|
|
3814
|
+
onClick: () => setHintIndex((index) => (index + 1) % HINT_KEYS.length),
|
|
3815
|
+
},
|
|
3816
|
+
t(HINT_KEYS[hintIndex % HINT_KEYS.length]),
|
|
3817
|
+
),
|
|
3081
3818
|
);
|
|
3082
3819
|
|
|
3083
3820
|
const toastNode = toast === null
|
|
@@ -3117,6 +3854,107 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
3117
3854
|
onPointerDown: onResizePointerDown,
|
|
3118
3855
|
});
|
|
3119
3856
|
|
|
3857
|
+
const previewNode = previewImage === null
|
|
3858
|
+
? null
|
|
3859
|
+
: h(
|
|
3860
|
+
'div',
|
|
3861
|
+
{
|
|
3862
|
+
className: 'dsh-np-preview',
|
|
3863
|
+
role: 'dialog',
|
|
3864
|
+
'aria-modal': 'true',
|
|
3865
|
+
'aria-label': t('imagePreview'),
|
|
3866
|
+
onClick: () => setPreviewImage(null),
|
|
3867
|
+
},
|
|
3868
|
+
h('img', {
|
|
3869
|
+
className: 'dsh-np-preview-img',
|
|
3870
|
+
src: imageUrl(previewImage.path, binding.cwd),
|
|
3871
|
+
alt: previewImage.name,
|
|
3872
|
+
onClick: (event) => event?.stopPropagation?.(),
|
|
3873
|
+
}),
|
|
3874
|
+
h(
|
|
3875
|
+
'div',
|
|
3876
|
+
{ className: 'dsh-np-preview-bar' },
|
|
3877
|
+
h(
|
|
3878
|
+
'button',
|
|
3879
|
+
{
|
|
3880
|
+
type: 'button',
|
|
3881
|
+
className: 'dsh-np-preview-btn',
|
|
3882
|
+
title: t('openOriginal'),
|
|
3883
|
+
'aria-label': t('openOriginal'),
|
|
3884
|
+
onClick: (event) => {
|
|
3885
|
+
event?.stopPropagation?.();
|
|
3886
|
+
openOriginalImage(previewImage);
|
|
3887
|
+
},
|
|
3888
|
+
},
|
|
3889
|
+
h(OpenIcon, { size: 15 }),
|
|
3890
|
+
),
|
|
3891
|
+
h(
|
|
3892
|
+
'button',
|
|
3893
|
+
{
|
|
3894
|
+
type: 'button',
|
|
3895
|
+
className: 'dsh-np-preview-btn',
|
|
3896
|
+
title: t('closePreview'),
|
|
3897
|
+
'aria-label': t('closePreview'),
|
|
3898
|
+
onClick: (event) => {
|
|
3899
|
+
event?.stopPropagation?.();
|
|
3900
|
+
setPreviewImage(null);
|
|
3901
|
+
},
|
|
3902
|
+
},
|
|
3903
|
+
h(CloseIcon, { size: 15 }),
|
|
3904
|
+
),
|
|
3905
|
+
),
|
|
3906
|
+
h('div', { className: 'dsh-np-preview-name' }, previewImage.name),
|
|
3907
|
+
);
|
|
3908
|
+
|
|
3909
|
+
const deleteTarget = deleteConfirm === ''
|
|
3910
|
+
? undefined
|
|
3911
|
+
: segments.find((segment) => segment.id === deleteConfirm && segment.archived !== true);
|
|
3912
|
+
const confirmNode = deleteTarget === undefined
|
|
3913
|
+
? null
|
|
3914
|
+
: h(
|
|
3915
|
+
'div',
|
|
3916
|
+
{
|
|
3917
|
+
className: 'dsh-np-confirm',
|
|
3918
|
+
role: 'alertdialog',
|
|
3919
|
+
'aria-modal': 'true',
|
|
3920
|
+
'aria-label': t('deleteConfirmTitle'),
|
|
3921
|
+
onClick: () => setDeleteConfirm(''),
|
|
3922
|
+
},
|
|
3923
|
+
h(
|
|
3924
|
+
'div',
|
|
3925
|
+
{
|
|
3926
|
+
className: 'dsh-np-confirm-card',
|
|
3927
|
+
onClick: (event) => event?.stopPropagation?.(),
|
|
3928
|
+
},
|
|
3929
|
+
h('div', { className: 'dsh-np-confirm-title' }, t('deleteConfirmTitle')),
|
|
3930
|
+
h('div', { className: 'dsh-np-confirm-body' }, t('deleteConfirmBody')),
|
|
3931
|
+
h('div', { className: 'dsh-np-confirm-hint' }, t('deleteConfirmHint')),
|
|
3932
|
+
h(
|
|
3933
|
+
'div',
|
|
3934
|
+
{ className: 'dsh-np-confirm-actions' },
|
|
3935
|
+
h(
|
|
3936
|
+
'button',
|
|
3937
|
+
{
|
|
3938
|
+
type: 'button',
|
|
3939
|
+
className: 'dsh-np-confirm-btn',
|
|
3940
|
+
onClick: () => setDeleteConfirm(''),
|
|
3941
|
+
},
|
|
3942
|
+
t('deleteConfirmCancel'),
|
|
3943
|
+
),
|
|
3944
|
+
h(
|
|
3945
|
+
'button',
|
|
3946
|
+
{
|
|
3947
|
+
type: 'button',
|
|
3948
|
+
className: 'dsh-np-confirm-btn',
|
|
3949
|
+
'data-danger': 'true',
|
|
3950
|
+
onClick: () => deleteActiveSegment(deleteTarget.id),
|
|
3951
|
+
},
|
|
3952
|
+
t('deleteConfirmOk'),
|
|
3953
|
+
),
|
|
3954
|
+
),
|
|
3955
|
+
),
|
|
3956
|
+
);
|
|
3957
|
+
|
|
3120
3958
|
const card = h(
|
|
3121
3959
|
'section',
|
|
3122
3960
|
{
|
|
@@ -3139,6 +3977,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
3139
3977
|
},
|
|
3140
3978
|
header,
|
|
3141
3979
|
body,
|
|
3980
|
+
previewNode,
|
|
3981
|
+
confirmNode,
|
|
3142
3982
|
resizeHandle,
|
|
3143
3983
|
);
|
|
3144
3984
|
|
|
@@ -3204,6 +4044,43 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
3204
4044
|
};
|
|
3205
4045
|
}, 'dsh-notepad: dictionaries');
|
|
3206
4046
|
|
|
4047
|
+
// Register the geometry-reset chord with DSH's shortcut service so it
|
|
4048
|
+
// reaches the page even when the host intercepts native key bindings.
|
|
4049
|
+
const installResetShortcut = (scope) => {
|
|
4050
|
+
const service = typeof scope.get === 'function' ? scope.get('shortcuts') : undefined;
|
|
4051
|
+
if (service === undefined || service === null || typeof service.register !== 'function') return;
|
|
4052
|
+
try {
|
|
4053
|
+
scope.effect(() => service.register({
|
|
4054
|
+
id: 'notepad.resetGeometry',
|
|
4055
|
+
label: () => translate('resetShortcut'),
|
|
4056
|
+
aliases: ['reset notepad', 'reset notepad geometry'],
|
|
4057
|
+
defaults: {
|
|
4058
|
+
'desktop:macos': { code: 'Digit0', modifiers: ['primary', 'shift'] },
|
|
4059
|
+
'desktop:windows': { code: 'Digit0', modifiers: ['primary', 'shift'] },
|
|
4060
|
+
'desktop:linux': { code: 'Digit0', modifiers: ['primary', 'shift'] },
|
|
4061
|
+
'web:macos': { code: 'Digit0', modifiers: ['primary', 'shift'] },
|
|
4062
|
+
'web:windows': { code: 'Digit0', modifiers: ['primary', 'shift'] },
|
|
4063
|
+
'web:linux': { code: 'Digit0', modifiers: ['primary', 'shift'] },
|
|
4064
|
+
},
|
|
4065
|
+
regions: ['page', 'editable'],
|
|
4066
|
+
modals: [],
|
|
4067
|
+
resolve: () => ({ status: 'handled', run: requestUiReset }),
|
|
4068
|
+
}), 'dsh-notepad: reset shortcut');
|
|
4069
|
+
resetShortcutHandledByHost = true;
|
|
4070
|
+
} catch (error) {
|
|
4071
|
+
console.warn('[dsh-notepad] reset shortcut registration failed:', error?.message ?? error);
|
|
4072
|
+
}
|
|
4073
|
+
};
|
|
4074
|
+
if (ctx.get('shortcuts') !== undefined) {
|
|
4075
|
+
installResetShortcut(ctx);
|
|
4076
|
+
} else if (typeof ctx.inject === 'function') {
|
|
4077
|
+
try {
|
|
4078
|
+
ctx.inject(['shortcuts'], (scoped) => installResetShortcut(scoped));
|
|
4079
|
+
} catch {
|
|
4080
|
+
/* The raw key listener and header button remain as fallbacks. */
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
|
|
3207
4084
|
const slots = ctx.get('slots');
|
|
3208
4085
|
if (slots === undefined || typeof slots.register !== 'function' || typeof slots.inject !== 'function') {
|
|
3209
4086
|
console.warn('[dsh-notepad] slots service missing; floating notepad disabled');
|