@restregular/dsh-notepad 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -50,6 +50,7 @@ window.__ModuleLoader__.load({
50
50
  const SAVE_DELAY = 500;
51
51
  const MAX_SEGMENTS = 200;
52
52
  const MAX_SEGMENT_CHARS = 160000;
53
+ const MAX_SEGMENT_HEIGHT = 240;
53
54
  const MAX_TITLE_CHARS = 200;
54
55
  const MAX_IMAGES = 20;
55
56
  const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
@@ -88,6 +89,10 @@ window.__ModuleLoader__.load({
88
89
  insert: '插入到输入框',
89
90
  insertSegment: '插入此段',
90
91
  insertSelected: '插入选中的 {n} 段',
92
+ insertNewSession: '在新会话中插入',
93
+ insertNewSelected: '在新会话中插入选中的 {n} 段',
94
+ insertNewDone: '已在新会话中填入内容',
95
+ insertNewUnavailable: '无法新建会话,内容已复制',
91
96
  inserted: '已插入输入框',
92
97
  insertEmpty: '没有可插入的内容',
93
98
  insertUnavailable: '输入框暂不可用,内容已复制',
@@ -99,6 +104,10 @@ window.__ModuleLoader__.load({
99
104
  send: '发送',
100
105
  sendSegment: '发送此段',
101
106
  sendSelected: '发送选中的 {n} 段',
107
+ sendNewSession: '在新会话中发送',
108
+ sendNewSelected: '在新会话中发送选中的 {n} 段',
109
+ sendNewDone: '已在新会话中发送',
110
+ sendNewUnavailable: '无法新建会话,内容已复制',
102
111
  sending: '发送中…',
103
112
  sent: '已发送',
104
113
  sendFailed: '发送失败',
@@ -147,6 +156,12 @@ window.__ModuleLoader__.load({
147
156
  restoredToast: '已恢复',
148
157
  restoredCount: '已恢复 {n} 段',
149
158
  deleteSegment: '删除此段',
159
+ deleteActiveSegment: '删除此段(不可恢复)',
160
+ deleteConfirmTitle: '删除这个分段?',
161
+ deleteConfirmBody: '删除后无法恢复。分段里的文字和图片引用会一起移除。',
162
+ deleteConfirmHint: '此操作不可撤销',
163
+ deleteConfirmCancel: '取消',
164
+ deleteConfirmOk: '删除',
150
165
  deletedToast: '已删除',
151
166
  clearArchive: '清空归档',
152
167
  clearedArchive: '已清空 {n} 段归档',
@@ -160,13 +175,24 @@ window.__ModuleLoader__.load({
160
175
  show: '显示记事本(Ctrl+Shift+N)',
161
176
  chars: '{n} 字符',
162
177
  segments: '{n} 段',
163
- hint: 'Enter 新建分段 · Shift+Enter 换行 · Ctrl+Enter 插入 · Ctrl+Shift+0 复位浮窗 · 支持粘贴图片',
178
+ hintNext: '点击查看下一条快捷键',
179
+ hintEnter: 'Enter 新建分段',
180
+ hintNewline: 'Shift+Enter 分段内换行',
181
+ hintInsert: 'Ctrl+Enter 插入当前分段',
182
+ hintToggle: 'Ctrl+Shift+N 显示 / 隐藏浮窗',
183
+ hintReset: 'Ctrl+Shift+R 复位浮窗',
184
+ hintArrow: '↑ / ↓ 切换分段',
185
+ hintPaste: '粘贴图片自动保存到工作区',
186
+ resetShortcut: '复位记事本位置和尺寸',
164
187
  workspace: '工作区',
165
188
  resize: '拖动调整大小',
166
189
  uiReset: '已复位浮窗位置和尺寸',
167
190
  addImage: '添加图片',
168
191
  removeImage: '移除图片',
169
192
  openImage: '打开图片',
193
+ imagePreview: '图片预览',
194
+ closePreview: '关闭预览',
195
+ openOriginal: '打开原图',
170
196
  imageAdded: '已添加 {n} 张图片',
171
197
  imageFailed: '图片添加失败',
172
198
  imageUnsupported: '只支持 PNG / JPEG / WebP / GIF 图片',
@@ -196,6 +222,10 @@ window.__ModuleLoader__.load({
196
222
  insert: 'Insert into composer',
197
223
  insertSegment: 'Insert this segment',
198
224
  insertSelected: 'Insert {n} selected',
225
+ insertNewSession: 'Insert into a new session',
226
+ insertNewSelected: 'Insert {n} selected into a new session',
227
+ insertNewDone: 'Filled into a new session',
228
+ insertNewUnavailable: 'Could not create a session; copied instead',
199
229
  inserted: 'Inserted into composer',
200
230
  insertEmpty: 'Nothing to insert',
201
231
  insertUnavailable: 'Composer unavailable; copied instead',
@@ -207,6 +237,10 @@ window.__ModuleLoader__.load({
207
237
  send: 'Send',
208
238
  sendSegment: 'Send this segment',
209
239
  sendSelected: 'Send {n} selected',
240
+ sendNewSession: 'Send into a new session',
241
+ sendNewSelected: 'Send {n} selected into a new session',
242
+ sendNewDone: 'Sent into a new session',
243
+ sendNewUnavailable: 'Could not create a session; copied instead',
210
244
  sending: 'Sending…',
211
245
  sent: 'Sent',
212
246
  sendFailed: 'Send failed',
@@ -255,6 +289,12 @@ window.__ModuleLoader__.load({
255
289
  restoredToast: 'Restored',
256
290
  restoredCount: 'Restored {n}',
257
291
  deleteSegment: 'Delete this segment',
292
+ deleteActiveSegment: 'Delete this segment (cannot be undone)',
293
+ deleteConfirmTitle: 'Delete this segment?',
294
+ deleteConfirmBody: 'This cannot be undone. The text and image references in the segment will be removed too.',
295
+ deleteConfirmHint: 'This action cannot be undone',
296
+ deleteConfirmCancel: 'Cancel',
297
+ deleteConfirmOk: 'Delete',
258
298
  deletedToast: 'Deleted',
259
299
  clearArchive: 'Clear archive',
260
300
  clearedArchive: 'Cleared {n} archived',
@@ -268,13 +308,24 @@ window.__ModuleLoader__.load({
268
308
  show: 'Show notepad (Ctrl+Shift+N)',
269
309
  chars: '{n} chars',
270
310
  segments: '{n} segments',
271
- hint: 'Enter new segment · Shift+Enter newline · Ctrl+Enter insert · Ctrl+Shift+0 reset window · Paste images',
311
+ hintNext: 'Click for the next shortcut',
312
+ hintEnter: 'Enter creates a segment',
313
+ hintNewline: 'Shift+Enter adds a line break',
314
+ hintInsert: 'Ctrl+Enter inserts the current segment',
315
+ hintToggle: 'Ctrl+Shift+N shows / hides the notepad',
316
+ hintReset: 'Ctrl+Shift+R resets the floating window',
317
+ hintArrow: '↑ / ↓ switch segments',
318
+ hintPaste: 'Pasted images are saved in the workspace',
319
+ resetShortcut: 'Reset the notepad position and size',
272
320
  workspace: 'Workspace',
273
321
  resize: 'Drag to resize',
274
322
  uiReset: 'Window position and size reset',
275
323
  addImage: 'Add image',
276
324
  removeImage: 'Remove image',
277
325
  openImage: 'Open image',
326
+ imagePreview: 'Image preview',
327
+ closePreview: 'Close preview',
328
+ openOriginal: 'Open original',
278
329
  imageAdded: 'Added {n} image(s)',
279
330
  imageFailed: 'Could not add image',
280
331
  imageUnsupported: 'Only PNG / JPEG / WebP / GIF images are supported',
@@ -287,6 +338,17 @@ window.__ModuleLoader__.load({
287
338
  },
288
339
  };
289
340
 
341
+ /** Rotating shortcut tips shown one at a time in the footer. */
342
+ const HINT_KEYS = [
343
+ 'hintEnter',
344
+ 'hintNewline',
345
+ 'hintInsert',
346
+ 'hintToggle',
347
+ 'hintReset',
348
+ 'hintArrow',
349
+ 'hintPaste',
350
+ ];
351
+
290
352
  function preferredLanguage() {
291
353
  try {
292
354
  const tag = String((typeof navigator !== 'undefined' && navigator.language) || 'zh').toLowerCase();
@@ -576,6 +638,60 @@ window.__ModuleLoader__.load({
576
638
  );
577
639
  }
578
640
 
641
+ /** Two panels, the second with a plus: start a new session. */
642
+ function NewSessionIcon({ size = 16 }) {
643
+ return h(
644
+ Svg,
645
+ { size, viewBox: '0 0 16 16' },
646
+ h('rect', {
647
+ x: 1.9, y: 3.2, width: 5.1, height: 9.4, rx: 1.4,
648
+ stroke: 'currentColor', strokeWidth: 1.25,
649
+ }),
650
+ h('rect', {
651
+ x: 8.7, y: 3.2, width: 5.4, height: 9.4, rx: 1.4,
652
+ stroke: 'currentColor', strokeWidth: 1.25,
653
+ }),
654
+ h('path', {
655
+ d: 'M11.4 6.1v3.8M9.5 8h3.8',
656
+ stroke: 'currentColor', strokeWidth: 1.25, strokeLinecap: 'round',
657
+ }),
658
+ );
659
+ }
660
+
661
+ /** Circular arrow: reset the floating window geometry. */
662
+ function ResetIcon({ size = 16 }) {
663
+ return h(
664
+ Svg,
665
+ { size, viewBox: '0 0 16 16' },
666
+ h('path', {
667
+ d: 'M13 8a5 5 0 1 1-1.6-3.7',
668
+ stroke: 'currentColor', strokeWidth: 1.35, strokeLinecap: 'round',
669
+ }),
670
+ h('path', {
671
+ d: 'M13.3 2.7v3.4H9.9',
672
+ stroke: 'currentColor', strokeWidth: 1.35,
673
+ strokeLinecap: 'round', strokeLinejoin: 'round',
674
+ }),
675
+ );
676
+ }
677
+
678
+ /** Arrow leaving a frame: open the original image. */
679
+ function OpenIcon({ size = 16 }) {
680
+ return h(
681
+ Svg,
682
+ { size, viewBox: '0 0 16 16' },
683
+ h('path', {
684
+ d: 'M9.4 3.2h3.4v3.4M12.8 3.2 7.6 8.4',
685
+ stroke: 'currentColor', strokeWidth: 1.3,
686
+ strokeLinecap: 'round', strokeLinejoin: 'round',
687
+ }),
688
+ h('path', {
689
+ 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',
690
+ stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round',
691
+ }),
692
+ );
693
+ }
694
+
579
695
  /** Two arrows converging: merge selected segments into one. */
580
696
  function MergeIcon({ size = 16 }) {
581
697
  return h(
@@ -636,7 +752,7 @@ window.__ModuleLoader__.load({
636
752
  .dsh-np-btn[data-accent=true]{background:var(--np-accent);color:var(--np-accent-fg)}
637
753
  .dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 88%,#000);color:var(--np-accent-fg)}
638
754
  .dsh-np-btn:disabled{opacity:.38;cursor:default;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280)}
639
- .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-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}
755
+ .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}
640
756
  .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}
641
757
  .dsh-np-card[data-minimized=true] .dsh-np-body{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,visibility 0s linear .16s}
642
758
  .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))}
@@ -669,7 +785,14 @@ window.__ModuleLoader__.load({
669
785
  .dsh-np-segbtn[data-ai=true]{color:var(--np-accent);opacity:1}
670
786
  .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))}
671
787
  .dsh-np-segbtn:disabled{opacity:.3;cursor:default}
672
- .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}
788
+ .dsh-np-actiongroup{display:inline-flex;align-items:center;flex:none;padding:1px;border:1px solid transparent;border-radius:8px;transition:border-color .12s ease .12s,background .12s ease .12s}
789
+ .dsh-np-actiongroup:hover{border-color:var(--dsw-alias-border-l2,rgb(15 23 42/.14));background:color-mix(in srgb,var(--np-surface-2) 58%,transparent);transition-delay:.18s}
790
+ .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);transition-delay:0s}
791
+ .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 .12s,opacity .12s ease .12s,visibility 0s linear .26s}
792
+ .dsh-np-actiongroup:hover .dsh-np-group-extra{width:24px!important;opacity:1;visibility:visible;pointer-events:auto;transition:width .14s ease .2s,opacity .12s ease .2s,visibility 0s linear .2s}
793
+ .dsh-np-actiongroup:focus-within .dsh-np-group-extra{width:24px!important;opacity:1;visibility:visible;pointer-events:auto;transition-delay:0s}
794
+ .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}
795
+ .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:hidden;white-space:pre-wrap;overflow-wrap:anywhere;tab-size:2}
673
796
  .dsh-np-seg-text::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2)}
674
797
  .dsh-np-seg-text:disabled{opacity:.6}
675
798
  .dsh-np-file{display:none}
@@ -706,6 +829,24 @@ window.__ModuleLoader__.load({
706
829
  .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}
707
830
  .dsh-np-count{flex:none;font-variant-numeric:tabular-nums}
708
831
  .dsh-np-hint{margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.85;font-size:10px}
832
+ button.dsh-np-hint{padding:0;border:0;background:none;color:inherit;font:inherit;font-size:10px;cursor:pointer;text-align:right}
833
+ button.dsh-np-hint:hover{opacity:1;color:var(--dsw-alias-label-secondary,#4b5563);text-decoration:underline}
834
+ .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}
835
+ .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)}
836
+ .dsh-np-preview-bar{position:absolute;top:8px;right:8px;display:flex;align-items:center;gap:4px}
837
+ .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)}
838
+ .dsh-np-preview-btn:hover{background:rgb(15 23 42/.82)}
839
+ .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}
840
+ .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}
841
+ .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))}
842
+ .dsh-np-confirm-title{font-size:13px;font-weight:650;color:var(--dsw-alias-label-primary,#1f2430)}
843
+ .dsh-np-confirm-body{margin-top:6px;color:var(--dsw-alias-label-secondary,#4b5563);font-size:12px;line-height:1.55}
844
+ .dsh-np-confirm-hint{margin-top:8px;color:var(--dsw-alias-state-error-primary,#e25555);font-size:11px}
845
+ .dsh-np-confirm-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:12px}
846
+ .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}
847
+ .dsh-np-confirm-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430)}
848
+ .dsh-np-confirm-btn[data-danger=true]{border-color:transparent;background:var(--dsw-alias-state-error-primary,#e25555);color:#fff}
849
+ .dsh-np-confirm-btn[data-danger=true]:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e25555) 86%,#000);color:#fff}
709
850
  .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}
710
851
  .dsh-np-toast-text{overflow:hidden;text-overflow:ellipsis}
711
852
  .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}
@@ -720,6 +861,7 @@ window.__ModuleLoader__.load({
720
861
  [data-row-key^="workspace:"][data-np-unsent]{position:relative}
721
862
  [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))}
722
863
  @keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
864
+ @keyframes dsh-np-fade{from{opacity:0}to{opacity:1}}
723
865
  @keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
724
866
  @keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
725
867
  @keyframes dsh-np-spin{to{transform:rotate(360deg)}}
@@ -727,7 +869,7 @@ body[data-ds-dark-theme] .dsh-np-card{--np-accent-fg:#0f1115;box-shadow:0 18px 4
727
869
  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))}
728
870
  body[data-ds-dark-theme] .dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 84%,#fff)}
729
871
  body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
730
- @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}}`;
872
+ @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}}`;
731
873
 
732
874
  // ------------------------------------------------------------------
733
875
  // Helpers
@@ -900,17 +1042,43 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
900
1042
  return count;
901
1043
  }
902
1044
 
903
- /** Whether a whole server payload predates per-segment creation times. */
904
- function isLegacyPayload(data) {
905
- const segments = Array.isArray(data?.segments) ? data.segments : [];
906
- return segments.length > 0 && segments.every((segment) =>
907
- segment !== null && typeof segment === 'object'
908
- && (typeof segment.createdAt !== 'string' || segment.createdAt === ''));
1045
+ /** A segment with no title, text, or images can be deleted without a prompt. */
1046
+ function isEmptySegment(segment) {
1047
+ if (segment === null || segment === undefined) return false;
1048
+ const title = typeof segment.title === 'string' ? segment.title.trim() : '';
1049
+ const text = typeof segment.text === 'string' ? segment.text.trim() : '';
1050
+ const images = Array.isArray(segment.images) ? segment.images : [];
1051
+ return title === '' && text === '' && images.length === 0;
1052
+ }
1053
+
1054
+ /** Merge image references in order, dropping duplicates and respecting the cap. */
1055
+ function collectImages(segments) {
1056
+ const seen = new Set();
1057
+ const images = [];
1058
+ for (const segment of segments) {
1059
+ for (const image of Array.isArray(segment?.images) ? segment.images : []) {
1060
+ if (seen.has(image.path) || images.length >= MAX_IMAGES) continue;
1061
+ seen.add(image.path);
1062
+ images.push(image);
1063
+ }
1064
+ }
1065
+ return images;
1066
+ }
1067
+
1068
+ /**
1069
+ * Decode a GET /note payload. Version 4 (0.4–0.6) stored newest-first;
1070
+ * version 5 stores oldest-first, so a v4 list is reversed once for display.
1071
+ */
1072
+ function readServerSegments(data) {
1073
+ let value = normalizeServerSegments(data);
1074
+ if (data?.version === 4) value = value.slice().reverse();
1075
+ return value;
909
1076
  }
910
1077
 
911
1078
  /**
912
1079
  * Adopt server content without moving cards the user is looking at.
913
- * Known ids keep the local order; cards created elsewhere land on top.
1080
+ * Known ids keep the local order; cards created elsewhere land at the
1081
+ * bottom, matching the oldest-to-newest display direction.
914
1082
  */
915
1083
  function mergeServerOrder(current, incoming) {
916
1084
  const byId = new Map();
@@ -922,7 +1090,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
922
1090
  }
923
1091
  const known = new Set(kept.map((segment) => segment.id));
924
1092
  const added = incoming.filter((segment) => !known.has(segment.id));
925
- return [...added, ...kept];
1093
+ return [...kept, ...added];
926
1094
  }
927
1095
 
928
1096
  function isAbsolutePath(value) {
@@ -1049,7 +1217,11 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1049
1217
  element.style.height = 'auto';
1050
1218
  const height = Number(element.scrollHeight);
1051
1219
  if (Number.isFinite(height) && height > 0) {
1052
- element.style.height = `${Math.min(240, Math.max(26, height))}px`;
1220
+ const capped = height > MAX_SEGMENT_HEIGHT;
1221
+ element.style.height = `${Math.min(MAX_SEGMENT_HEIGHT, Math.max(26, height))}px`;
1222
+ // Keep the native scrollbar hidden until the card really reaches
1223
+ // its limit; otherwise CSS overflow:auto can reserve a gutter early.
1224
+ element.style.overflowY = capped ? 'auto' : 'hidden';
1053
1225
  }
1054
1226
  } catch {
1055
1227
  /* A non-layout shim (tests) may not expose scrollHeight. */
@@ -1252,6 +1424,38 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1252
1424
  }
1253
1425
  }
1254
1426
 
1427
+ /** Mounted notepads register here so a host shortcut can reset them. */
1428
+ const resetHandlers = new Set();
1429
+
1430
+ /** Whether the host shortcut service already owns Ctrl/Cmd+Shift+0. */
1431
+ let resetShortcutHandledByHost = false;
1432
+
1433
+ /** Ask every mounted notepad to reset its position and size. */
1434
+ function requestUiReset() {
1435
+ let handled = false;
1436
+ for (const handler of resetHandlers) {
1437
+ try {
1438
+ handler();
1439
+ handled = true;
1440
+ } catch {
1441
+ /* One failing overlay must not block the others. */
1442
+ }
1443
+ }
1444
+ return handled;
1445
+ }
1446
+
1447
+ /** Read the General Settings "send behavior while busy" preference. */
1448
+ function busyEnterPreference(ctx) {
1449
+ try {
1450
+ const forms = ctx.get('configForms') ?? ctx.configForms;
1451
+ const form = typeof forms?.get === 'function' ? forms.get('ui-conversation') : undefined;
1452
+ const value = form?.getSnapshot?.()?.value;
1453
+ return value?.busyEnter === 'steer' ? 'steer' : 'queue';
1454
+ } catch {
1455
+ return 'queue';
1456
+ }
1457
+ }
1458
+
1255
1459
  // ------------------------------------------------------------------
1256
1460
  // Overlay
1257
1461
  // ------------------------------------------------------------------
@@ -1280,6 +1484,9 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1280
1484
  const [segmentUndo, setSegmentUndo] = useState(() => new Map());
1281
1485
  const [batchUndo, setBatchUndo] = useState(null);
1282
1486
  const [titleEditing, setTitleEditing] = useState('');
1487
+ const [previewImage, setPreviewImage] = useState(null);
1488
+ const [deleteConfirm, setDeleteConfirm] = useState('');
1489
+ const [hintIndex, setHintIndex] = useState(0);
1283
1490
  const [wsCounts, setWsCounts] = useState({});
1284
1491
  const [currentWsId, setCurrentWsId] = useState('');
1285
1492
 
@@ -1420,6 +1627,24 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1420
1627
  toastTimerRef.current = setTimeout(() => setToast(null), action === undefined || action === null ? 1800 : 5000);
1421
1628
  };
1422
1629
 
1630
+ const resetGeometry = () => {
1631
+ setUi((previous) => clampUi({
1632
+ ...previous,
1633
+ x: null,
1634
+ y: null,
1635
+ width: DEFAULT_W,
1636
+ height: DEFAULT_H,
1637
+ }));
1638
+ showToast(t('uiReset'));
1639
+ };
1640
+
1641
+ useEffect(() => {
1642
+ resetHandlers.add(resetGeometry);
1643
+ return () => {
1644
+ resetHandlers.delete(resetGeometry);
1645
+ };
1646
+ }, []);
1647
+
1423
1648
  const scheduleSave = () => {
1424
1649
  clearTimer();
1425
1650
  timerRef.current = setTimeout(() => {
@@ -1485,12 +1710,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1485
1710
  setSegmentUndo(new Map());
1486
1711
  setBatchUndoState(null);
1487
1712
  setTitleEditing('');
1713
+ setPreviewImage(null);
1714
+ setDeleteConfirm('');
1488
1715
  pendingScrollRef.current = null;
1489
1716
  setStatus('idle');
1490
1717
  setError('');
1491
1718
  return undefined;
1492
1719
  }
1493
- if (dirtyRef.current && rootRef.current !== '' && rootRef.current !== binding.cwd) {
1720
+ if (dirtyRef.current && rootRef.current !== '') {
1721
+ // Flush the old workspace/session note before this binding replaces
1722
+ // the in-memory segments; a same-workspace session switch must not
1723
+ // drop a still-debounced edit.
1494
1724
  saveNowRef.current?.({ force: true });
1495
1725
  }
1496
1726
  clearTimer();
@@ -1505,6 +1735,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1505
1735
  setSegmentUndo(new Map());
1506
1736
  setBatchUndoState(null);
1507
1737
  setTitleEditing('');
1738
+ setPreviewImage(null);
1739
+ setDeleteConfirm('');
1508
1740
  pendingScrollRef.current = null;
1509
1741
  setStatus('loading');
1510
1742
  setError('');
@@ -1516,10 +1748,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1516
1748
  fetchNote(root, binding.id)
1517
1749
  .then((data) => {
1518
1750
  if (cancelled || rootRef.current !== root || mySeq !== loadSeqRef.current) return;
1519
- let value = normalizeServerSegments(data);
1520
- // Notes written before 0.4 appended new cards at the bottom;
1521
- // reverse that legacy order once so the newest card leads.
1522
- if (isLegacyPayload(data)) value = value.slice().reverse();
1751
+ let value = readServerSegments(data);
1523
1752
  if (value.length === 0) value = [newSegment('')];
1524
1753
  segmentsRef.current = value;
1525
1754
  setSegments(value);
@@ -1686,16 +1915,10 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1686
1915
  setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
1687
1916
  return;
1688
1917
  }
1689
- if (event.key === '0' || event.code === 'Digit0' || event.code === 'Numpad0') {
1918
+ if (!resetShortcutHandledByHost
1919
+ && (event.key === 'R' || event.key === 'r' || event.code === 'KeyR')) {
1690
1920
  event.preventDefault();
1691
- setUi((previous) => clampUi({
1692
- ...previous,
1693
- x: null,
1694
- y: null,
1695
- width: DEFAULT_W,
1696
- height: DEFAULT_H,
1697
- }));
1698
- showToast(t('uiReset'));
1921
+ resetGeometry();
1699
1922
  }
1700
1923
  };
1701
1924
  window.addEventListener('keydown', onKeyDown, true);
@@ -1708,6 +1931,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1708
1931
  };
1709
1932
  }, []);
1710
1933
 
1934
+ // Close the preview / delete confirmation with Escape ----------------
1935
+ useEffect(() => {
1936
+ if (previewImage === null && deleteConfirm === '') return undefined;
1937
+ const onKeyDown = (event) => {
1938
+ if (event.key !== 'Escape') return;
1939
+ event.preventDefault();
1940
+ setPreviewImage(null);
1941
+ setDeleteConfirm('');
1942
+ };
1943
+ window.addEventListener('keydown', onKeyDown, true);
1944
+ return () => window.removeEventListener('keydown', onKeyDown, true);
1945
+ }, [previewImage, deleteConfirm]);
1946
+
1711
1947
  // Flush on page hide / reload -----------------------------------------
1712
1948
  useEffect(() => {
1713
1949
  const flush = () => {
@@ -1758,8 +1994,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1758
1994
  pendingScrollRef.current = null;
1759
1995
  return;
1760
1996
  }
1761
- let incoming = normalizeServerSegments(data);
1762
- if (isLegacyPayload(data)) incoming = incoming.slice().reverse();
1997
+ let incoming = readServerSegments(data);
1763
1998
  if (incoming.length === 0) incoming = [newSegment('')];
1764
1999
  const value = mergeServerOrder(segmentsRef.current, incoming);
1765
2000
  segmentsRef.current = value;
@@ -1936,6 +2171,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1936
2171
  const stillReferenced = next.some((segment) =>
1937
2172
  (Array.isArray(segment.images) ? segment.images : []).some((image) => image.path === pathValue));
1938
2173
  commitSegments(next);
2174
+ if (previewImage !== null && previewImage.path === pathValue) setPreviewImage(null);
1939
2175
  if (!stillReferenced) deleteImageFile(pathValue);
1940
2176
  };
1941
2177
 
@@ -1966,7 +2202,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1966
2202
  const files = clipboardImageFiles(data);
1967
2203
  if (files.length === 0) return;
1968
2204
  event.preventDefault?.();
1969
- event.stopPropagation?.();
2205
+ event?.stopPropagation?.();
1970
2206
  attachImages(id, files, 'paste');
1971
2207
  };
1972
2208
 
@@ -1975,7 +2211,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1975
2211
  setDragSegment('');
1976
2212
  if (files.length === 0) return;
1977
2213
  event.preventDefault?.();
1978
- event.stopPropagation?.();
2214
+ event?.stopPropagation?.();
1979
2215
  attachImages(id, files, 'pick');
1980
2216
  };
1981
2217
 
@@ -1995,11 +2231,15 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1995
2231
  };
1996
2232
 
1997
2233
  const openImage = (image) => {
2234
+ setPreviewImage({ path: image.path, name: image.name });
2235
+ };
2236
+
2237
+ const openOriginalImage = (image) => {
1998
2238
  const url = imageUrl(image.path, rootRef.current);
1999
2239
  try {
2000
2240
  window.open(url, '_blank', 'noopener,noreferrer');
2001
2241
  } catch {
2002
- /* Popups may be blocked; the path remains available in the title. */
2242
+ /* Popups may be blocked; the in-panel preview still works. */
2003
2243
  }
2004
2244
  };
2005
2245
 
@@ -2009,7 +2249,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2009
2249
  return;
2010
2250
  }
2011
2251
  const segment = newSegment('');
2012
- commitSegments([segment, ...segmentsRef.current], { focus: { id: segment.id, caret: 0 } });
2252
+ commitSegments([...segmentsRef.current, segment], { focus: { id: segment.id, caret: 0 } });
2013
2253
  };
2014
2254
 
2015
2255
  const splitSegment = (id, element) => {
@@ -2021,19 +2261,13 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2021
2261
  const nextSegment = newSegment(tail);
2022
2262
  const updated = segmentsRef.current.map((segment) =>
2023
2263
  (segment.id === id ? { ...segment, text: head } : segment));
2024
- // Enter at the end creates a fresh card, which belongs at the top.
2025
- // Splitting in the middle keeps the tail next to its source card so
2026
- // the caret does not jump away from what the user is editing.
2027
- const next = tail.trim() === ''
2028
- ? [nextSegment, ...updated]
2029
- : (() => {
2030
- const out = [];
2031
- for (const segment of updated) {
2032
- out.push(segment);
2033
- if (segment.id === id) out.push(nextSegment);
2034
- }
2035
- return out;
2036
- })();
2264
+ // Cards are displayed oldest-first, so the split tail always follows
2265
+ // its source card (also for an empty tail at the end of the note).
2266
+ const next = [];
2267
+ for (const segment of updated) {
2268
+ next.push(segment);
2269
+ if (segment.id === id) next.push(nextSegment);
2270
+ }
2037
2271
  commitSegments(next, { focus: { id: nextSegment.id, caret: 0 } });
2038
2272
  };
2039
2273
 
@@ -2102,6 +2336,28 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2102
2336
  showToast(t('deletedToast'), { label: t('undo'), onClick: undoLast });
2103
2337
  };
2104
2338
 
2339
+ /** Irreversible delete for an active segment, gated by a confirmation. */
2340
+ const deleteActiveSegment = (id) => {
2341
+ setDeleteConfirm('');
2342
+ setPreviewImage(null);
2343
+ const target = segmentsRef.current.find((segment) => segment.id === id && segment.archived !== true);
2344
+ if (target === undefined) return;
2345
+ // This delete cannot be undone: retire any older snackbar undo so it
2346
+ // cannot resurrect the segment through a stale snapshot.
2347
+ undoRef.current = null;
2348
+ setToast(null);
2349
+ setSegmentUndo((previous) => {
2350
+ if (!previous.has(id)) return previous;
2351
+ const updated = new Map(previous);
2352
+ updated.delete(id);
2353
+ return updated;
2354
+ });
2355
+ const next = segmentsRef.current.filter((segment) => segment.id !== id);
2356
+ commitSegments(next);
2357
+ scheduleImageCleanup(target.images);
2358
+ showToast(t('deletedToast'));
2359
+ };
2360
+
2105
2361
  const clearArchive = () => {
2106
2362
  const archived = segmentsRef.current.filter((segment) => segment.archived === true);
2107
2363
  if (archived.length === 0) return;
@@ -2129,15 +2385,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2129
2385
  for (let index = 0; index < firstIndex; index += 1) {
2130
2386
  if (!ids.has(segmentsRef.current[index].id)) insertAt += 1;
2131
2387
  }
2132
- const seenPaths = new Set();
2133
- const images = [];
2134
- for (const segment of chosen) {
2135
- for (const image of Array.isArray(segment.images) ? segment.images : []) {
2136
- if (seenPaths.has(image.path) || images.length >= MAX_IMAGES) continue;
2137
- seenPaths.add(image.path);
2138
- images.push(image);
2139
- }
2140
- }
2388
+ const images = collectImages(chosen);
2141
2389
  const merged = {
2142
2390
  id: newSegmentId(),
2143
2391
  text: chosen.map((segment) => segment.text).filter((text) => text.trim() !== '').join('\n\n'),
@@ -2199,7 +2447,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2199
2447
  const types = event?.dataTransfer?.types;
2200
2448
  if (types !== undefined && types !== null && !Array.from(types).includes('application/x-dsh-notepad-segment')) return;
2201
2449
  event.preventDefault?.();
2202
- event.stopPropagation?.();
2450
+ event?.stopPropagation?.();
2203
2451
  const rect = typeof event.currentTarget?.getBoundingClientRect === 'function'
2204
2452
  ? event.currentTarget.getBoundingClientRect()
2205
2453
  : null;
@@ -2224,7 +2472,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2224
2472
  setReorderTarget(null);
2225
2473
  if (source === '') return;
2226
2474
  event.preventDefault?.();
2227
- event.stopPropagation?.();
2475
+ event?.stopPropagation?.();
2228
2476
  reorderSegments(source, id, target?.after === true);
2229
2477
  };
2230
2478
 
@@ -2447,6 +2695,28 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2447
2695
  showToast(copied ? t('insertUnavailable') : t('copyFailed'));
2448
2696
  };
2449
2697
 
2698
+ /** Start a new session in this workspace with the text already in its composer. */
2699
+ const insertNewSession = async (list) => {
2700
+ const value = mergedText(list);
2701
+ if (value === '') {
2702
+ showToast(t('insertEmpty'));
2703
+ return;
2704
+ }
2705
+ const uiWorkspace = ctx.get('uiWorkspace');
2706
+ if (uiWorkspace !== undefined && uiWorkspace !== null && typeof uiWorkspace.startSession === 'function') {
2707
+ try {
2708
+ if (dirtyRef.current) await saveNowRef.current?.({ force: true });
2709
+ uiWorkspace.startSession(undefined, { prompt: value, clearPreviousDraft: true });
2710
+ showToast(t('insertNewDone'));
2711
+ return;
2712
+ } catch {
2713
+ /* Fall through to copy. */
2714
+ }
2715
+ }
2716
+ const copied = await copyText(value);
2717
+ showToast(copied ? t('insertNewUnavailable') : t('copyFailed'));
2718
+ };
2719
+
2450
2720
  const replaceSegments = async (list) => {
2451
2721
  const value = mergedText(list);
2452
2722
  if (value === '') {
@@ -2461,55 +2731,93 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2461
2731
  showToast(copied ? t('replaceUnavailable') : t('copyFailed'));
2462
2732
  };
2463
2733
 
2734
+ /** Resolve queue vs steer exactly like the composer's Send button. */
2735
+ const resolveSendMode = (sessionId) => {
2736
+ try {
2737
+ const sessions = ctx.get('sessions');
2738
+ const row = sessions?.list?.getSnapshot?.()?.byId?.[sessionId];
2739
+ if (row?.running !== true) return 'queue';
2740
+ } catch {
2741
+ return 'queue';
2742
+ }
2743
+ return busyEnterPreference(ctx);
2744
+ };
2745
+
2746
+ /** Send through the session face with an explicit queue/steer mode. */
2747
+ const promptSession = async (sessionId, value, mode) => {
2748
+ const sessions = ctx.get('sessions');
2749
+ if (sessions === undefined || sessions === null || typeof sessions.binding !== 'function') return false;
2750
+ const liveBinding = sessions.binding(sessionId);
2751
+ const session = liveBinding?.session;
2752
+ if (session === undefined || session === null || typeof session.prompt !== 'function') return false;
2753
+ let echo;
2754
+ try {
2755
+ echo = typeof session.beginSubmission === 'function'
2756
+ ? session.beginSubmission({ mode, text: value, attachments: [] })
2757
+ : undefined;
2758
+ } catch {
2759
+ echo = undefined;
2760
+ }
2761
+ try {
2762
+ const result = await session.prompt([{ type: 'text', text: value }], mode, undefined, echo?.requestId);
2763
+ if (result !== undefined && result !== null && result.ok === false) {
2764
+ const failure = new Error(result.error?.message ?? 'prompt failed');
2765
+ failure.code = result.error?.code;
2766
+ throw failure;
2767
+ }
2768
+ return true;
2769
+ } catch (reason) {
2770
+ try {
2771
+ echo?.abandon?.();
2772
+ } catch {
2773
+ /* ignore */
2774
+ }
2775
+ throw reason;
2776
+ }
2777
+ };
2778
+
2464
2779
  /**
2465
- * Send text into the current session. Prefers the documented scoped
2466
- * conversation service; falls back to the session face with a local echo.
2780
+ * Send text into a session. Queue prefers the documented scoped conversation
2781
+ * service; steer and its fallback go through the session face. The delivery
2782
+ * mode follows the General Settings "send while busy" preference.
2467
2783
  * @returns {Promise<boolean>} whether a prompt was accepted
2468
2784
  */
2469
- const sendToSession = async (value) => {
2470
- const sessionId = rootSessionRef.current !== '' ? rootSessionRef.current : bindingRef.current?.id;
2785
+ const sendToSession = async (value, targetSessionId) => {
2786
+ const sessionId = typeof targetSessionId === 'string' && targetSessionId !== ''
2787
+ ? targetSessionId
2788
+ : (rootSessionRef.current !== '' ? rootSessionRef.current : bindingRef.current?.id);
2471
2789
  if (typeof sessionId !== 'string' || sessionId === '') return false;
2472
2790
  const sessions = ctx.get('sessions');
2473
2791
  if (sessions === undefined || sessions === null) return false;
2792
+ const mode = resolveSendMode(sessionId);
2793
+ let scoped;
2474
2794
  if (typeof sessions.scope === 'function') {
2475
- const scoped = sessions.scope(sessionId);
2476
- const conversation = scoped === undefined || scoped === null
2477
- ? undefined
2478
- : typeof scoped.get === 'function' ? scoped.get('conversation') : scoped.conversation;
2479
- if (conversation !== undefined && conversation !== null && typeof conversation.send === 'function') {
2480
- await conversation.send(value);
2481
- return true;
2795
+ try {
2796
+ scoped = sessions.scope(sessionId);
2797
+ } catch {
2798
+ scoped = undefined;
2482
2799
  }
2483
2800
  }
2484
- if (typeof sessions.binding === 'function') {
2485
- const liveBinding = sessions.binding(sessionId);
2486
- const session = liveBinding?.session;
2487
- if (session !== undefined && session !== null && typeof session.prompt === 'function') {
2488
- let echo;
2489
- try {
2490
- echo = typeof session.beginSubmission === 'function'
2491
- ? session.beginSubmission({ mode: 'queue', text: value, attachments: [] })
2492
- : undefined;
2493
- } catch {
2494
- echo = undefined;
2495
- }
2496
- try {
2497
- const result = await session.prompt([{ type: 'text', text: value }], 'queue', undefined, echo?.requestId);
2498
- if (result !== undefined && result !== null && result.ok === false) {
2499
- throw new Error(result.error?.message ?? 'prompt failed');
2500
- }
2801
+ const conversation = scoped === undefined || scoped === null
2802
+ ? undefined
2803
+ : typeof scoped.get === 'function' ? scoped.get('conversation') : scoped.conversation;
2804
+ if (mode === 'queue' && conversation !== undefined && conversation !== null
2805
+ && typeof conversation.send === 'function') {
2806
+ await conversation.send(value);
2807
+ return true;
2808
+ }
2809
+ try {
2810
+ return await promptSession(sessionId, value, mode);
2811
+ } catch (reason) {
2812
+ if (mode === 'steer' && reason?.code === 'session/steer-unavailable') {
2813
+ if (conversation !== undefined && conversation !== null && typeof conversation.send === 'function') {
2814
+ await conversation.send(value);
2501
2815
  return true;
2502
- } catch (reason) {
2503
- try {
2504
- echo?.abandon?.();
2505
- } catch {
2506
- /* ignore */
2507
- }
2508
- throw reason;
2509
2816
  }
2817
+ return promptSession(sessionId, value, 'queue');
2510
2818
  }
2819
+ throw reason;
2511
2820
  }
2512
- return false;
2513
2821
  };
2514
2822
 
2515
2823
  const sendSegments = async (list) => {
@@ -2550,8 +2858,78 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2550
2858
  }
2551
2859
  };
2552
2860
 
2861
+ /** Create an empty session in a workspace and return its id. */
2862
+ const createWorkspaceSession = async (cwd) => {
2863
+ const sessions = ctx.get('sessions');
2864
+ if (sessions === undefined || sessions === null || typeof sessions.create !== 'function') return null;
2865
+ const result = await sessions.create({ cwd });
2866
+ if (result === undefined || result === null) return null;
2867
+ if (result.ok === false) {
2868
+ const failure = new Error(result.error?.message ?? 'create session failed');
2869
+ failure.code = result.error?.code;
2870
+ throw failure;
2871
+ }
2872
+ const id = result.value?.sessionId ?? result.sessionId
2873
+ ?? (typeof result === 'string' ? result : undefined);
2874
+ return typeof id === 'string' && id !== '' ? id : null;
2875
+ };
2876
+
2877
+ /** Start a fresh session, send the text there, then switch to it. */
2878
+ const sendNewSession = async (list) => {
2879
+ if (sendingRef.current) return;
2880
+ const value = mergedText(list);
2881
+ if (value === '') {
2882
+ showToast(t('insertEmpty'));
2883
+ return;
2884
+ }
2885
+ const cwd = bindingRef.current?.cwd ?? rootRef.current;
2886
+ if (cwd === '') {
2887
+ showToast(t('sendNewUnavailable'));
2888
+ return;
2889
+ }
2890
+ sendingRef.current = true;
2891
+ setSending(true);
2892
+ try {
2893
+ const newSessionId = await createWorkspaceSession(cwd);
2894
+ if (newSessionId === null) {
2895
+ if (!insertValue(value)) await copyText(value);
2896
+ showToast(t('sendNewUnavailable'));
2897
+ return;
2898
+ }
2899
+ const accepted = await sendToSession(value, newSessionId);
2900
+ if (accepted !== true) {
2901
+ if (!insertValue(value)) await copyText(value);
2902
+ showToast(t('sendUnavailable'));
2903
+ return;
2904
+ }
2905
+ showToast(t('sendNewDone'));
2906
+ const ids = new Set(
2907
+ list.filter((segment) => segment.archived !== true).map((segment) => segment.id),
2908
+ );
2909
+ if (ids.size > 0 && (rootRef.current === '' || rootRef.current === cwd)) {
2910
+ const archivedAt = new Date().toISOString();
2911
+ const next = segmentsRef.current.map((segment) =>
2912
+ (ids.has(segment.id) ? { ...segment, archived: true, archivedAt } : segment));
2913
+ setSelection(new Set([...selectedRef.current].filter((id) => !ids.has(id))));
2914
+ commitSegments(next);
2915
+ await saveNowRef.current?.({ force: true });
2916
+ }
2917
+ const uiWorkspace = ctx.get('uiWorkspace');
2918
+ if (uiWorkspace !== undefined && uiWorkspace !== null && typeof uiWorkspace.openSession === 'function') {
2919
+ uiWorkspace.openSession(newSessionId);
2920
+ }
2921
+ } catch (reason) {
2922
+ showToast(`${t('sendFailed')}: ${String(reason?.message ?? reason)}`);
2923
+ } finally {
2924
+ sendingRef.current = false;
2925
+ setSending(false);
2926
+ }
2927
+ };
2928
+
2553
2929
  const copyAll = async () => {
2554
- const value = mergedText(segmentsRef.current);
2930
+ // Copy only active work: archived segments were already sent or filed
2931
+ // away and must not leak back into a copy-all.
2932
+ const value = mergedText(segmentsRef.current.filter((segment) => segment.archived !== true));
2555
2933
  if (value === '') {
2556
2934
  showToast(t('insertEmpty'));
2557
2935
  return;
@@ -2613,6 +2991,43 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2613
2991
  return true;
2614
2992
  };
2615
2993
 
2994
+ /** Remove a content-empty segment and keep a nearby typing surface. */
2995
+ const deleteEmptySegment = (id, previous, next) => {
2996
+ const remaining = segmentsRef.current.filter((segment) => segment.id !== id);
2997
+ const activeRemaining = remaining.filter((segment) => segment.archived !== true);
2998
+ if (activeRemaining.length === 0) {
2999
+ const fresh = newSegment('');
3000
+ commitSegments([fresh], { focus: { id: fresh.id, caret: 0 } });
3001
+ return;
3002
+ }
3003
+ const focus = previous ?? next;
3004
+ if (focus === undefined) {
3005
+ commitSegments(remaining);
3006
+ return;
3007
+ }
3008
+ const caret = previous !== undefined ? previous.text.length : 0;
3009
+ commitSegments(remaining, { focus: { id: focus.id, caret } });
3010
+ };
3011
+
3012
+ /** Backspace at position 0 joins this segment onto the previous card. */
3013
+ const mergeWithPrevious = (previous, current) => {
3014
+ const title = [previous.title, current.title]
3015
+ .map((value) => (typeof value === 'string' ? value.trim() : ''))
3016
+ .filter((value) => value !== '')
3017
+ .join(' · ')
3018
+ .slice(0, MAX_TITLE_CHARS);
3019
+ const merged = {
3020
+ ...previous,
3021
+ text: `${previous.text}${current.text}`,
3022
+ title,
3023
+ images: collectImages([previous, current]),
3024
+ };
3025
+ const next = segmentsRef.current
3026
+ .filter((segment) => segment.id !== current.id)
3027
+ .map((segment) => (segment.id === previous.id ? merged : segment));
3028
+ commitSegments(next, { focus: { id: previous.id, caret: previous.text.length } });
3029
+ };
3030
+
2616
3031
  const onSegmentKeyDown = (event, id) => {
2617
3032
  if (event.isComposing === true || event.nativeEvent?.isComposing === true) return;
2618
3033
  const chord = event.ctrlKey || event.metaKey;
@@ -2649,6 +3064,27 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2649
3064
  }
2650
3065
  return;
2651
3066
  }
3067
+ if (event.key === 'Backspace') {
3068
+ if (event.altKey || event.shiftKey || chord) return;
3069
+ const element = event.currentTarget ?? event.target;
3070
+ const start = typeof element?.selectionStart === 'number' ? element.selectionStart : null;
3071
+ const end = typeof element?.selectionEnd === 'number' ? element.selectionEnd : start;
3072
+ if (start !== 0 || end !== 0) return;
3073
+ const active = segmentsRef.current.filter((segment) => segment.archived !== true);
3074
+ const position = active.findIndex((segment) => segment.id === id);
3075
+ const current = position < 0 ? undefined : active[position];
3076
+ if (current === undefined) return;
3077
+ const previous = position > 0 ? active[position - 1] : undefined;
3078
+ if (isEmptySegment(current)) {
3079
+ event.preventDefault?.();
3080
+ deleteEmptySegment(current.id, previous, active[position + 1]);
3081
+ return;
3082
+ }
3083
+ if (previous === undefined) return;
3084
+ event.preventDefault?.();
3085
+ mergeWithPrevious(previous, current);
3086
+ return;
3087
+ }
2652
3088
  if (event.key !== 'Enter' || event.shiftKey || event.altKey || chord) return;
2653
3089
  event.preventDefault();
2654
3090
  splitSegment(id, event.currentTarget ?? event.target);
@@ -2688,7 +3124,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2688
3124
  if (ui.minimized) return;
2689
3125
  if (event.button !== undefined && event.button !== 0) return;
2690
3126
  event.preventDefault?.();
2691
- event.stopPropagation?.();
3127
+ event?.stopPropagation?.();
2692
3128
  const start = { px: event.clientX, py: event.clientY, width: ui.width, height: ui.height };
2693
3129
  resizeRef.current = start;
2694
3130
  setResizing(true);
@@ -2734,6 +3170,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2734
3170
  const allSelected = activeSegments.length > 0 && selectedCount === activeSegments.length;
2735
3171
  const unsentCount = countUnsent(segments);
2736
3172
  const totalChars = segments.reduce((sum, segment) => sum + segment.text.length, 0);
3173
+ const activeChars = activeSegments.reduce((sum, segment) => sum + segment.text.length, 0);
2737
3174
  const displayWidth = ui.width;
2738
3175
  const displayHeight = ui.minimized ? HEAD_H : ui.height;
2739
3176
  const fallbackX = Math.max(EDGE_GAP, ((typeof window !== 'undefined' && window.innerWidth) || 1280) - displayWidth - 24);
@@ -2761,6 +3198,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2761
3198
  h('span', { className: 'dsh-np-title' }, t('title')),
2762
3199
  h('span', { className: 'dsh-np-workspace', title: binding.cwd }, workspaceName(binding.cwd)),
2763
3200
  h('span', { className: 'dsh-np-spacer' }),
3201
+ h(
3202
+ 'button',
3203
+ {
3204
+ type: 'button',
3205
+ className: 'dsh-np-btn',
3206
+ title: `${t('resetShortcut')} · Ctrl+Shift+R`,
3207
+ 'aria-label': t('resetShortcut'),
3208
+ onClick: resetGeometry,
3209
+ },
3210
+ h(ResetIcon, { size: 15 }),
3211
+ ),
2764
3212
  h(
2765
3213
  'button',
2766
3214
  {
@@ -2768,7 +3216,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2768
3216
  className: 'dsh-np-btn',
2769
3217
  title: t('copy'),
2770
3218
  'aria-label': t('copy'),
2771
- disabled: totalChars === 0,
3219
+ disabled: activeChars === 0,
2772
3220
  onClick: copyAll,
2773
3221
  },
2774
3222
  h(CopyIcon, { size: 15 }),
@@ -2821,6 +3269,35 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2821
3269
  ),
2822
3270
  );
2823
3271
 
3272
+ /** One icon that expands into current-session + new-session actions. */
3273
+ const actionGroup = ({ size, primary, extra, disabled = false }) => {
3274
+ const base = size === 'bar' ? 'dsh-np-btn' : 'dsh-np-segbtn';
3275
+ const primaryProps = {
3276
+ type: 'button',
3277
+ className: base,
3278
+ title: primary.title,
3279
+ 'aria-label': primary.title,
3280
+ disabled,
3281
+ onClick: primary.onClick,
3282
+ };
3283
+ if (primary.accent === true) primaryProps['data-accent'] = 'true';
3284
+ const extraProps = {
3285
+ type: 'button',
3286
+ className: `${base} dsh-np-group-extra`,
3287
+ title: extra.title,
3288
+ 'aria-label': extra.title,
3289
+ disabled,
3290
+ onClick: extra.onClick,
3291
+ };
3292
+ if (extra.danger === true) extraProps['data-danger'] = 'true';
3293
+ return h(
3294
+ 'div',
3295
+ { className: 'dsh-np-actiongroup', 'data-size': size },
3296
+ h('button', primaryProps, primary.icon),
3297
+ h('button', extraProps, extra.icon ?? h(NewSessionIcon, { size: 14 })),
3298
+ );
3299
+ };
3300
+
2824
3301
  const tools = h(
2825
3302
  'div',
2826
3303
  { className: 'dsh-np-tools' },
@@ -2838,18 +3315,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2838
3315
  selectedCount > 0 ? t('selected', { n: selectedCount }) : t('selectHint')),
2839
3316
  ),
2840
3317
  h('span', { className: 'dsh-np-spacer' }),
2841
- h(
2842
- 'button',
2843
- {
2844
- type: 'button',
2845
- className: 'dsh-np-btn',
3318
+ actionGroup({
3319
+ size: 'bar',
3320
+ disabled: selectedCount === 0,
3321
+ primary: {
2846
3322
  title: selectedCount > 0 ? t('insertSelected', { n: selectedCount }) : t('insert'),
2847
- 'aria-label': t('insert'),
2848
- disabled: selectedCount === 0,
2849
3323
  onClick: () => insertSegments(selectedActive),
3324
+ icon: h(InsertIcon, { size: 14 }),
2850
3325
  },
2851
- h(InsertIcon, { size: 14 }),
2852
- ),
3326
+ extra: {
3327
+ title: selectedCount > 0 ? t('insertNewSelected', { n: selectedCount }) : t('insertNewSession'),
3328
+ onClick: () => insertNewSession(selectedActive),
3329
+ },
3330
+ }),
2853
3331
  h(
2854
3332
  'button',
2855
3333
  {
@@ -2862,21 +3340,22 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2862
3340
  },
2863
3341
  h(ReplaceIcon, { size: 14 }),
2864
3342
  ),
2865
- h(
2866
- 'button',
2867
- {
2868
- type: 'button',
2869
- className: 'dsh-np-btn',
2870
- 'data-accent': 'true',
3343
+ actionGroup({
3344
+ size: 'bar',
3345
+ disabled: selectedCount === 0 || sending,
3346
+ primary: {
2871
3347
  title: selectedCount > 0
2872
3348
  ? (sending ? t('sending') : t('sendSelected', { n: selectedCount }))
2873
3349
  : t('send'),
2874
- 'aria-label': t('send'),
2875
- disabled: selectedCount === 0 || sending,
2876
3350
  onClick: () => sendSegments(selectedActive),
3351
+ icon: h(SendIcon, { size: 14 }),
3352
+ accent: true,
2877
3353
  },
2878
- h(SendIcon, { size: 14 }),
2879
- ),
3354
+ extra: {
3355
+ title: selectedCount > 0 ? t('sendNewSelected', { n: selectedCount }) : t('sendNewSession'),
3356
+ onClick: () => sendNewSession(selectedActive),
3357
+ },
3358
+ }),
2880
3359
  h(
2881
3360
  'button',
2882
3361
  {
@@ -3109,7 +3588,6 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3109
3588
  'aria-label': t('segmentTitle'),
3110
3589
  onClick: () => setTitleEditing(segment.id),
3111
3590
  },
3112
- t('titlePlaceholder'),
3113
3591
  ),
3114
3592
  h(
3115
3593
  'button',
@@ -3122,18 +3600,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3122
3600
  },
3123
3601
  h(ImageIcon, { size: 14 }),
3124
3602
  ),
3125
- h(
3126
- 'button',
3127
- {
3128
- type: 'button',
3129
- className: 'dsh-np-segbtn',
3603
+ actionGroup({
3604
+ size: 'card',
3605
+ disabled: empty,
3606
+ primary: {
3130
3607
  title: t('insertSegment'),
3131
- 'aria-label': t('insertSegment'),
3132
- disabled: empty,
3133
3608
  onClick: () => insertSegments([segment]),
3609
+ icon: h(InsertIcon, { size: 14 }),
3134
3610
  },
3135
- h(InsertIcon, { size: 14 }),
3136
- ),
3611
+ extra: {
3612
+ title: t('insertNewSession'),
3613
+ onClick: () => insertNewSession([segment]),
3614
+ },
3615
+ }),
3137
3616
  h(
3138
3617
  'button',
3139
3618
  {
@@ -3146,18 +3625,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3146
3625
  },
3147
3626
  h(ReplaceIcon, { size: 14 }),
3148
3627
  ),
3149
- h(
3150
- 'button',
3151
- {
3152
- type: 'button',
3153
- className: 'dsh-np-segbtn',
3628
+ actionGroup({
3629
+ size: 'card',
3630
+ disabled: empty || sending,
3631
+ primary: {
3154
3632
  title: t('sendSegment'),
3155
- 'aria-label': t('sendSegment'),
3156
- disabled: empty || sending,
3157
3633
  onClick: () => sendSegments([segment]),
3634
+ icon: h(SendIcon, { size: 14 }),
3158
3635
  },
3159
- h(SendIcon, { size: 14 }),
3160
- ),
3636
+ extra: {
3637
+ title: t('sendNewSession'),
3638
+ onClick: () => sendNewSession([segment]),
3639
+ },
3640
+ }),
3161
3641
  h(
3162
3642
  'button',
3163
3643
  {
@@ -3175,17 +3655,23 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3175
3655
  ? h('span', { className: 'dsh-np-mini-spinner' })
3176
3656
  : canUndoOptimize ? h(UndoIcon, { size: 14 }) : h(SparkleIcon, { size: 14 }),
3177
3657
  ),
3178
- h(
3179
- 'button',
3180
- {
3181
- type: 'button',
3182
- className: 'dsh-np-segbtn',
3658
+ actionGroup({
3659
+ size: 'card',
3660
+ primary: {
3183
3661
  title: t('archiveSegment'),
3184
- 'aria-label': t('archiveSegment'),
3185
3662
  onClick: () => archiveSegment(segment.id),
3663
+ icon: h(ArchiveIcon, { size: 14 }),
3186
3664
  },
3187
- h(ArchiveIcon, { size: 14 }),
3188
- ),
3665
+ extra: {
3666
+ title: t('deleteActiveSegment'),
3667
+ danger: true,
3668
+ onClick: () => {
3669
+ if (isEmptySegment(segment)) deleteActiveSegment(segment.id);
3670
+ else setDeleteConfirm(segment.id);
3671
+ },
3672
+ icon: h(TrashIcon, { size: 14 }),
3673
+ },
3674
+ }),
3189
3675
  ),
3190
3676
  h(
3191
3677
  'div',
@@ -3210,7 +3696,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3210
3696
  className: 'dsh-np-seg-text',
3211
3697
  rows: 1,
3212
3698
  value: segment.text,
3213
- placeholder: index === 0 ? t('placeholder') : t('segmentPlaceholder'),
3699
+ placeholder: index === activeSegments.length - 1 ? t('placeholder') : t('segmentPlaceholder'),
3214
3700
  spellCheck: false,
3215
3701
  maxLength: MAX_SEGMENT_CHARS,
3216
3702
  'aria-label': t('editor'),
@@ -3382,7 +3868,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3382
3868
  h('span', { className: 'dsh-np-count' },
3383
3869
  `${t('chars', { n: totalChars })} · ${t('segments', { n: activeSegments.length })}`
3384
3870
  + (totalImages > 0 ? ` · ${t('images', { n: totalImages })}` : '')),
3385
- h('span', { className: 'dsh-np-hint', title: t('hint') }, t('hint')),
3871
+ h(
3872
+ 'button',
3873
+ {
3874
+ type: 'button',
3875
+ className: 'dsh-np-hint',
3876
+ title: t('hintNext'),
3877
+ 'aria-label': t('hintNext'),
3878
+ onClick: () => setHintIndex((index) => (index + 1) % HINT_KEYS.length),
3879
+ },
3880
+ t(HINT_KEYS[hintIndex % HINT_KEYS.length]),
3881
+ ),
3386
3882
  );
3387
3883
 
3388
3884
  const toastNode = toast === null
@@ -3422,6 +3918,107 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3422
3918
  onPointerDown: onResizePointerDown,
3423
3919
  });
3424
3920
 
3921
+ const previewNode = previewImage === null
3922
+ ? null
3923
+ : h(
3924
+ 'div',
3925
+ {
3926
+ className: 'dsh-np-preview',
3927
+ role: 'dialog',
3928
+ 'aria-modal': 'true',
3929
+ 'aria-label': t('imagePreview'),
3930
+ onClick: () => setPreviewImage(null),
3931
+ },
3932
+ h('img', {
3933
+ className: 'dsh-np-preview-img',
3934
+ src: imageUrl(previewImage.path, binding.cwd),
3935
+ alt: previewImage.name,
3936
+ onClick: (event) => event?.stopPropagation?.(),
3937
+ }),
3938
+ h(
3939
+ 'div',
3940
+ { className: 'dsh-np-preview-bar' },
3941
+ h(
3942
+ 'button',
3943
+ {
3944
+ type: 'button',
3945
+ className: 'dsh-np-preview-btn',
3946
+ title: t('openOriginal'),
3947
+ 'aria-label': t('openOriginal'),
3948
+ onClick: (event) => {
3949
+ event?.stopPropagation?.();
3950
+ openOriginalImage(previewImage);
3951
+ },
3952
+ },
3953
+ h(OpenIcon, { size: 15 }),
3954
+ ),
3955
+ h(
3956
+ 'button',
3957
+ {
3958
+ type: 'button',
3959
+ className: 'dsh-np-preview-btn',
3960
+ title: t('closePreview'),
3961
+ 'aria-label': t('closePreview'),
3962
+ onClick: (event) => {
3963
+ event?.stopPropagation?.();
3964
+ setPreviewImage(null);
3965
+ },
3966
+ },
3967
+ h(CloseIcon, { size: 15 }),
3968
+ ),
3969
+ ),
3970
+ h('div', { className: 'dsh-np-preview-name' }, previewImage.name),
3971
+ );
3972
+
3973
+ const deleteTarget = deleteConfirm === ''
3974
+ ? undefined
3975
+ : segments.find((segment) => segment.id === deleteConfirm && segment.archived !== true);
3976
+ const confirmNode = deleteTarget === undefined
3977
+ ? null
3978
+ : h(
3979
+ 'div',
3980
+ {
3981
+ className: 'dsh-np-confirm',
3982
+ role: 'alertdialog',
3983
+ 'aria-modal': 'true',
3984
+ 'aria-label': t('deleteConfirmTitle'),
3985
+ onClick: () => setDeleteConfirm(''),
3986
+ },
3987
+ h(
3988
+ 'div',
3989
+ {
3990
+ className: 'dsh-np-confirm-card',
3991
+ onClick: (event) => event?.stopPropagation?.(),
3992
+ },
3993
+ h('div', { className: 'dsh-np-confirm-title' }, t('deleteConfirmTitle')),
3994
+ h('div', { className: 'dsh-np-confirm-body' }, t('deleteConfirmBody')),
3995
+ h('div', { className: 'dsh-np-confirm-hint' }, t('deleteConfirmHint')),
3996
+ h(
3997
+ 'div',
3998
+ { className: 'dsh-np-confirm-actions' },
3999
+ h(
4000
+ 'button',
4001
+ {
4002
+ type: 'button',
4003
+ className: 'dsh-np-confirm-btn',
4004
+ onClick: () => setDeleteConfirm(''),
4005
+ },
4006
+ t('deleteConfirmCancel'),
4007
+ ),
4008
+ h(
4009
+ 'button',
4010
+ {
4011
+ type: 'button',
4012
+ className: 'dsh-np-confirm-btn',
4013
+ 'data-danger': 'true',
4014
+ onClick: () => deleteActiveSegment(deleteTarget.id),
4015
+ },
4016
+ t('deleteConfirmOk'),
4017
+ ),
4018
+ ),
4019
+ ),
4020
+ );
4021
+
3425
4022
  const card = h(
3426
4023
  'section',
3427
4024
  {
@@ -3444,6 +4041,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3444
4041
  },
3445
4042
  header,
3446
4043
  body,
4044
+ previewNode,
4045
+ confirmNode,
3447
4046
  resizeHandle,
3448
4047
  );
3449
4048
 
@@ -3509,6 +4108,43 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3509
4108
  };
3510
4109
  }, 'dsh-notepad: dictionaries');
3511
4110
 
4111
+ // Register the geometry-reset chord with DSH's shortcut service so it
4112
+ // reaches the page even when the host intercepts native key bindings.
4113
+ const installResetShortcut = (scope) => {
4114
+ const service = typeof scope.get === 'function' ? scope.get('shortcuts') : undefined;
4115
+ if (service === undefined || service === null || typeof service.register !== 'function') return;
4116
+ try {
4117
+ scope.effect(() => service.register({
4118
+ id: 'notepad.resetGeometry',
4119
+ label: () => translate('resetShortcut'),
4120
+ aliases: ['reset notepad', 'reset notepad geometry'],
4121
+ defaults: {
4122
+ 'desktop:macos': { code: 'KeyR', modifiers: ['primary', 'shift'] },
4123
+ 'desktop:windows': { code: 'KeyR', modifiers: ['primary', 'shift'] },
4124
+ 'desktop:linux': { code: 'KeyR', modifiers: ['primary', 'shift'] },
4125
+ 'web:macos': { code: 'KeyR', modifiers: ['primary', 'shift'] },
4126
+ 'web:windows': { code: 'KeyR', modifiers: ['primary', 'shift'] },
4127
+ 'web:linux': { code: 'KeyR', modifiers: ['primary', 'shift'] },
4128
+ },
4129
+ regions: ['page', 'editable'],
4130
+ modals: [],
4131
+ resolve: () => ({ status: 'handled', run: requestUiReset }),
4132
+ }), 'dsh-notepad: reset shortcut');
4133
+ resetShortcutHandledByHost = true;
4134
+ } catch (error) {
4135
+ console.warn('[dsh-notepad] reset shortcut registration failed:', error?.message ?? error);
4136
+ }
4137
+ };
4138
+ if (ctx.get('shortcuts') !== undefined) {
4139
+ installResetShortcut(ctx);
4140
+ } else if (typeof ctx.inject === 'function') {
4141
+ try {
4142
+ ctx.inject(['shortcuts'], (scoped) => installResetShortcut(scoped));
4143
+ } catch {
4144
+ /* The raw key listener and header button remain as fallbacks. */
4145
+ }
4146
+ }
4147
+
3512
4148
  const slots = ctx.get('slots');
3513
4149
  if (slots === undefined || typeof slots.register !== 'function' || typeof slots.inject !== 'function') {
3514
4150
  console.warn('[dsh-notepad] slots service missing; floating notepad disabled');