@restregular/dsh-notepad 0.5.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/client.js CHANGED
@@ -88,6 +88,10 @@ window.__ModuleLoader__.load({
88
88
  insert: '插入到输入框',
89
89
  insertSegment: '插入此段',
90
90
  insertSelected: '插入选中的 {n} 段',
91
+ insertNewSession: '在新会话中插入',
92
+ insertNewSelected: '在新会话中插入选中的 {n} 段',
93
+ insertNewDone: '已在新会话中填入内容',
94
+ insertNewUnavailable: '无法新建会话,内容已复制',
91
95
  inserted: '已插入输入框',
92
96
  insertEmpty: '没有可插入的内容',
93
97
  insertUnavailable: '输入框暂不可用,内容已复制',
@@ -99,6 +103,10 @@ window.__ModuleLoader__.load({
99
103
  send: '发送',
100
104
  sendSegment: '发送此段',
101
105
  sendSelected: '发送选中的 {n} 段',
106
+ sendNewSession: '在新会话中发送',
107
+ sendNewSelected: '在新会话中发送选中的 {n} 段',
108
+ sendNewDone: '已在新会话中发送',
109
+ sendNewUnavailable: '无法新建会话,内容已复制',
102
110
  sending: '发送中…',
103
111
  sent: '已发送',
104
112
  sendFailed: '发送失败',
@@ -147,6 +155,12 @@ window.__ModuleLoader__.load({
147
155
  restoredToast: '已恢复',
148
156
  restoredCount: '已恢复 {n} 段',
149
157
  deleteSegment: '删除此段',
158
+ deleteActiveSegment: '删除此段(不可恢复)',
159
+ deleteConfirmTitle: '删除这个分段?',
160
+ deleteConfirmBody: '删除后无法恢复。分段里的文字和图片引用会一起移除。',
161
+ deleteConfirmHint: '此操作不可撤销',
162
+ deleteConfirmCancel: '取消',
163
+ deleteConfirmOk: '删除',
150
164
  deletedToast: '已删除',
151
165
  clearArchive: '清空归档',
152
166
  clearedArchive: '已清空 {n} 段归档',
@@ -160,13 +174,24 @@ window.__ModuleLoader__.load({
160
174
  show: '显示记事本(Ctrl+Shift+N)',
161
175
  chars: '{n} 字符',
162
176
  segments: '{n} 段',
163
- hint: 'Enter 新建分段 · Shift+Enter 换行 · Ctrl+Enter 插入 · Ctrl+Shift+0 复位浮窗 · 支持粘贴图片',
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: '复位记事本位置和尺寸',
164
186
  workspace: '工作区',
165
187
  resize: '拖动调整大小',
166
188
  uiReset: '已复位浮窗位置和尺寸',
167
189
  addImage: '添加图片',
168
190
  removeImage: '移除图片',
169
191
  openImage: '打开图片',
192
+ imagePreview: '图片预览',
193
+ closePreview: '关闭预览',
194
+ openOriginal: '打开原图',
170
195
  imageAdded: '已添加 {n} 张图片',
171
196
  imageFailed: '图片添加失败',
172
197
  imageUnsupported: '只支持 PNG / JPEG / WebP / GIF 图片',
@@ -196,6 +221,10 @@ window.__ModuleLoader__.load({
196
221
  insert: 'Insert into composer',
197
222
  insertSegment: 'Insert this segment',
198
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',
199
228
  inserted: 'Inserted into composer',
200
229
  insertEmpty: 'Nothing to insert',
201
230
  insertUnavailable: 'Composer unavailable; copied instead',
@@ -207,6 +236,10 @@ window.__ModuleLoader__.load({
207
236
  send: 'Send',
208
237
  sendSegment: 'Send this segment',
209
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',
210
243
  sending: 'Sending…',
211
244
  sent: 'Sent',
212
245
  sendFailed: 'Send failed',
@@ -255,6 +288,12 @@ window.__ModuleLoader__.load({
255
288
  restoredToast: 'Restored',
256
289
  restoredCount: 'Restored {n}',
257
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',
258
297
  deletedToast: 'Deleted',
259
298
  clearArchive: 'Clear archive',
260
299
  clearedArchive: 'Cleared {n} archived',
@@ -268,13 +307,24 @@ window.__ModuleLoader__.load({
268
307
  show: 'Show notepad (Ctrl+Shift+N)',
269
308
  chars: '{n} chars',
270
309
  segments: '{n} segments',
271
- hint: 'Enter new segment · Shift+Enter newline · Ctrl+Enter insert · Ctrl+Shift+0 reset window · Paste images',
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',
272
319
  workspace: 'Workspace',
273
320
  resize: 'Drag to resize',
274
321
  uiReset: 'Window position and size reset',
275
322
  addImage: 'Add image',
276
323
  removeImage: 'Remove image',
277
324
  openImage: 'Open image',
325
+ imagePreview: 'Image preview',
326
+ closePreview: 'Close preview',
327
+ openOriginal: 'Open original',
278
328
  imageAdded: 'Added {n} image(s)',
279
329
  imageFailed: 'Could not add image',
280
330
  imageUnsupported: 'Only PNG / JPEG / WebP / GIF images are supported',
@@ -287,6 +337,17 @@ window.__ModuleLoader__.load({
287
337
  },
288
338
  };
289
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
+
290
351
  function preferredLanguage() {
291
352
  try {
292
353
  const tag = String((typeof navigator !== 'undefined' && navigator.language) || 'zh').toLowerCase();
@@ -576,6 +637,60 @@ window.__ModuleLoader__.load({
576
637
  );
577
638
  }
578
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
+
579
694
  /** Two arrows converging: merge selected segments into one. */
580
695
  function MergeIcon({ size = 16 }) {
581
696
  return h(
@@ -636,7 +751,7 @@ window.__ModuleLoader__.load({
636
751
  .dsh-np-btn[data-accent=true]{background:var(--np-accent);color:var(--np-accent-fg)}
637
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)}
638
753
  .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}
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}
640
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}
641
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}
642
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))}
@@ -669,6 +784,11 @@ window.__ModuleLoader__.load({
669
784
  .dsh-np-segbtn[data-ai=true]{color:var(--np-accent);opacity:1}
670
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))}
671
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}
672
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}
673
793
  .dsh-np-seg-text::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2)}
674
794
  .dsh-np-seg-text:disabled{opacity:.6}
@@ -706,6 +826,24 @@ window.__ModuleLoader__.load({
706
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}
707
827
  .dsh-np-count{flex:none;font-variant-numeric:tabular-nums}
708
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}
709
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}
710
848
  .dsh-np-toast-text{overflow:hidden;text-overflow:ellipsis}
711
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}
@@ -720,6 +858,7 @@ window.__ModuleLoader__.load({
720
858
  [data-row-key^="workspace:"][data-np-unsent]{position:relative}
721
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))}
722
860
  @keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
861
+ @keyframes dsh-np-fade{from{opacity:0}to{opacity:1}}
723
862
  @keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
724
863
  @keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
725
864
  @keyframes dsh-np-spin{to{transform:rotate(360deg)}}
@@ -727,7 +866,7 @@ body[data-ds-dark-theme] .dsh-np-card{--np-accent-fg:#0f1115;box-shadow:0 18px 4
727
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))}
728
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)}
729
868
  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}}`;
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}}`;
731
870
 
732
871
  // ------------------------------------------------------------------
733
872
  // Helpers
@@ -1252,6 +1391,38 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1252
1391
  }
1253
1392
  }
1254
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
+
1255
1426
  // ------------------------------------------------------------------
1256
1427
  // Overlay
1257
1428
  // ------------------------------------------------------------------
@@ -1280,6 +1451,9 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1280
1451
  const [segmentUndo, setSegmentUndo] = useState(() => new Map());
1281
1452
  const [batchUndo, setBatchUndo] = useState(null);
1282
1453
  const [titleEditing, setTitleEditing] = useState('');
1454
+ const [previewImage, setPreviewImage] = useState(null);
1455
+ const [deleteConfirm, setDeleteConfirm] = useState('');
1456
+ const [hintIndex, setHintIndex] = useState(0);
1283
1457
  const [wsCounts, setWsCounts] = useState({});
1284
1458
  const [currentWsId, setCurrentWsId] = useState('');
1285
1459
 
@@ -1420,6 +1594,24 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1420
1594
  toastTimerRef.current = setTimeout(() => setToast(null), action === undefined || action === null ? 1800 : 5000);
1421
1595
  };
1422
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
+
1423
1615
  const scheduleSave = () => {
1424
1616
  clearTimer();
1425
1617
  timerRef.current = setTimeout(() => {
@@ -1485,12 +1677,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1485
1677
  setSegmentUndo(new Map());
1486
1678
  setBatchUndoState(null);
1487
1679
  setTitleEditing('');
1680
+ setPreviewImage(null);
1681
+ setDeleteConfirm('');
1488
1682
  pendingScrollRef.current = null;
1489
1683
  setStatus('idle');
1490
1684
  setError('');
1491
1685
  return undefined;
1492
1686
  }
1493
- if (dirtyRef.current && rootRef.current !== '' && rootRef.current !== binding.cwd) {
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.
1494
1691
  saveNowRef.current?.({ force: true });
1495
1692
  }
1496
1693
  clearTimer();
@@ -1505,6 +1702,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1505
1702
  setSegmentUndo(new Map());
1506
1703
  setBatchUndoState(null);
1507
1704
  setTitleEditing('');
1705
+ setPreviewImage(null);
1706
+ setDeleteConfirm('');
1508
1707
  pendingScrollRef.current = null;
1509
1708
  setStatus('loading');
1510
1709
  setError('');
@@ -1686,16 +1885,11 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1686
1885
  setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
1687
1886
  return;
1688
1887
  }
1689
- if (event.key === '0' || event.code === 'Digit0' || event.code === 'Numpad0') {
1888
+ if (!resetShortcutHandledByHost
1889
+ && (event.key === '0' || event.key === ')' || event.code === 'Digit0'
1890
+ || event.code === 'Numpad0' || event.keyCode === 48)) {
1690
1891
  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'));
1892
+ resetGeometry();
1699
1893
  }
1700
1894
  };
1701
1895
  window.addEventListener('keydown', onKeyDown, true);
@@ -1708,6 +1902,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1708
1902
  };
1709
1903
  }, []);
1710
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
+
1711
1918
  // Flush on page hide / reload -----------------------------------------
1712
1919
  useEffect(() => {
1713
1920
  const flush = () => {
@@ -1936,6 +2143,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1936
2143
  const stillReferenced = next.some((segment) =>
1937
2144
  (Array.isArray(segment.images) ? segment.images : []).some((image) => image.path === pathValue));
1938
2145
  commitSegments(next);
2146
+ if (previewImage !== null && previewImage.path === pathValue) setPreviewImage(null);
1939
2147
  if (!stillReferenced) deleteImageFile(pathValue);
1940
2148
  };
1941
2149
 
@@ -1966,7 +2174,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1966
2174
  const files = clipboardImageFiles(data);
1967
2175
  if (files.length === 0) return;
1968
2176
  event.preventDefault?.();
1969
- event.stopPropagation?.();
2177
+ event?.stopPropagation?.();
1970
2178
  attachImages(id, files, 'paste');
1971
2179
  };
1972
2180
 
@@ -1975,7 +2183,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1975
2183
  setDragSegment('');
1976
2184
  if (files.length === 0) return;
1977
2185
  event.preventDefault?.();
1978
- event.stopPropagation?.();
2186
+ event?.stopPropagation?.();
1979
2187
  attachImages(id, files, 'pick');
1980
2188
  };
1981
2189
 
@@ -1995,11 +2203,15 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
1995
2203
  };
1996
2204
 
1997
2205
  const openImage = (image) => {
2206
+ setPreviewImage({ path: image.path, name: image.name });
2207
+ };
2208
+
2209
+ const openOriginalImage = (image) => {
1998
2210
  const url = imageUrl(image.path, rootRef.current);
1999
2211
  try {
2000
2212
  window.open(url, '_blank', 'noopener,noreferrer');
2001
2213
  } catch {
2002
- /* Popups may be blocked; the path remains available in the title. */
2214
+ /* Popups may be blocked; the in-panel preview still works. */
2003
2215
  }
2004
2216
  };
2005
2217
 
@@ -2102,6 +2314,28 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2102
2314
  showToast(t('deletedToast'), { label: t('undo'), onClick: undoLast });
2103
2315
  };
2104
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
+
2105
2339
  const clearArchive = () => {
2106
2340
  const archived = segmentsRef.current.filter((segment) => segment.archived === true);
2107
2341
  if (archived.length === 0) return;
@@ -2199,7 +2433,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2199
2433
  const types = event?.dataTransfer?.types;
2200
2434
  if (types !== undefined && types !== null && !Array.from(types).includes('application/x-dsh-notepad-segment')) return;
2201
2435
  event.preventDefault?.();
2202
- event.stopPropagation?.();
2436
+ event?.stopPropagation?.();
2203
2437
  const rect = typeof event.currentTarget?.getBoundingClientRect === 'function'
2204
2438
  ? event.currentTarget.getBoundingClientRect()
2205
2439
  : null;
@@ -2224,7 +2458,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2224
2458
  setReorderTarget(null);
2225
2459
  if (source === '') return;
2226
2460
  event.preventDefault?.();
2227
- event.stopPropagation?.();
2461
+ event?.stopPropagation?.();
2228
2462
  reorderSegments(source, id, target?.after === true);
2229
2463
  };
2230
2464
 
@@ -2447,6 +2681,28 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2447
2681
  showToast(copied ? t('insertUnavailable') : t('copyFailed'));
2448
2682
  };
2449
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
+
2450
2706
  const replaceSegments = async (list) => {
2451
2707
  const value = mergedText(list);
2452
2708
  if (value === '') {
@@ -2461,55 +2717,93 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2461
2717
  showToast(copied ? t('replaceUnavailable') : t('copyFailed'));
2462
2718
  };
2463
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
+
2464
2765
  /**
2465
- * Send text into the current session. Prefers the documented scoped
2466
- * conversation service; falls back to the session face with a local echo.
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.
2467
2769
  * @returns {Promise<boolean>} whether a prompt was accepted
2468
2770
  */
2469
- const sendToSession = async (value) => {
2470
- const sessionId = rootSessionRef.current !== '' ? rootSessionRef.current : bindingRef.current?.id;
2771
+ const sendToSession = async (value, targetSessionId) => {
2772
+ const sessionId = typeof targetSessionId === 'string' && targetSessionId !== ''
2773
+ ? targetSessionId
2774
+ : (rootSessionRef.current !== '' ? rootSessionRef.current : bindingRef.current?.id);
2471
2775
  if (typeof sessionId !== 'string' || sessionId === '') return false;
2472
2776
  const sessions = ctx.get('sessions');
2473
2777
  if (sessions === undefined || sessions === null) return false;
2778
+ const mode = resolveSendMode(sessionId);
2779
+ let scoped;
2474
2780
  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;
2781
+ try {
2782
+ scoped = sessions.scope(sessionId);
2783
+ } catch {
2784
+ scoped = undefined;
2482
2785
  }
2483
2786
  }
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
- }
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);
2501
2801
  return true;
2502
- } catch (reason) {
2503
- try {
2504
- echo?.abandon?.();
2505
- } catch {
2506
- /* ignore */
2507
- }
2508
- throw reason;
2509
2802
  }
2803
+ return promptSession(sessionId, value, 'queue');
2510
2804
  }
2805
+ throw reason;
2511
2806
  }
2512
- return false;
2513
2807
  };
2514
2808
 
2515
2809
  const sendSegments = async (list) => {
@@ -2550,8 +2844,78 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2550
2844
  }
2551
2845
  };
2552
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
+
2553
2915
  const copyAll = async () => {
2554
- const value = mergedText(segmentsRef.current);
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));
2555
2919
  if (value === '') {
2556
2920
  showToast(t('insertEmpty'));
2557
2921
  return;
@@ -2688,7 +3052,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2688
3052
  if (ui.minimized) return;
2689
3053
  if (event.button !== undefined && event.button !== 0) return;
2690
3054
  event.preventDefault?.();
2691
- event.stopPropagation?.();
3055
+ event?.stopPropagation?.();
2692
3056
  const start = { px: event.clientX, py: event.clientY, width: ui.width, height: ui.height };
2693
3057
  resizeRef.current = start;
2694
3058
  setResizing(true);
@@ -2734,6 +3098,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2734
3098
  const allSelected = activeSegments.length > 0 && selectedCount === activeSegments.length;
2735
3099
  const unsentCount = countUnsent(segments);
2736
3100
  const totalChars = segments.reduce((sum, segment) => sum + segment.text.length, 0);
3101
+ const activeChars = activeSegments.reduce((sum, segment) => sum + segment.text.length, 0);
2737
3102
  const displayWidth = ui.width;
2738
3103
  const displayHeight = ui.minimized ? HEAD_H : ui.height;
2739
3104
  const fallbackX = Math.max(EDGE_GAP, ((typeof window !== 'undefined' && window.innerWidth) || 1280) - displayWidth - 24);
@@ -2761,6 +3126,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2761
3126
  h('span', { className: 'dsh-np-title' }, t('title')),
2762
3127
  h('span', { className: 'dsh-np-workspace', title: binding.cwd }, workspaceName(binding.cwd)),
2763
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
+ ),
2764
3140
  h(
2765
3141
  'button',
2766
3142
  {
@@ -2768,7 +3144,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2768
3144
  className: 'dsh-np-btn',
2769
3145
  title: t('copy'),
2770
3146
  'aria-label': t('copy'),
2771
- disabled: totalChars === 0,
3147
+ disabled: activeChars === 0,
2772
3148
  onClick: copyAll,
2773
3149
  },
2774
3150
  h(CopyIcon, { size: 15 }),
@@ -2821,6 +3197,37 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2821
3197
  ),
2822
3198
  );
2823
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
+
2824
3231
  const tools = h(
2825
3232
  'div',
2826
3233
  { className: 'dsh-np-tools' },
@@ -2838,18 +3245,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2838
3245
  selectedCount > 0 ? t('selected', { n: selectedCount }) : t('selectHint')),
2839
3246
  ),
2840
3247
  h('span', { className: 'dsh-np-spacer' }),
2841
- h(
2842
- 'button',
2843
- {
2844
- type: 'button',
2845
- className: 'dsh-np-btn',
3248
+ actionGroup({
3249
+ size: 'bar',
3250
+ disabled: selectedCount === 0,
3251
+ primary: {
2846
3252
  title: selectedCount > 0 ? t('insertSelected', { n: selectedCount }) : t('insert'),
2847
- 'aria-label': t('insert'),
2848
- disabled: selectedCount === 0,
2849
3253
  onClick: () => insertSegments(selectedActive),
3254
+ icon: h(InsertIcon, { size: 14 }),
2850
3255
  },
2851
- h(InsertIcon, { size: 14 }),
2852
- ),
3256
+ extra: {
3257
+ title: selectedCount > 0 ? t('insertNewSelected', { n: selectedCount }) : t('insertNewSession'),
3258
+ onClick: () => insertNewSession(selectedActive),
3259
+ },
3260
+ }),
2853
3261
  h(
2854
3262
  'button',
2855
3263
  {
@@ -2862,21 +3270,22 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
2862
3270
  },
2863
3271
  h(ReplaceIcon, { size: 14 }),
2864
3272
  ),
2865
- h(
2866
- 'button',
2867
- {
2868
- type: 'button',
2869
- className: 'dsh-np-btn',
2870
- 'data-accent': 'true',
3273
+ actionGroup({
3274
+ size: 'bar',
3275
+ disabled: selectedCount === 0 || sending,
3276
+ primary: {
2871
3277
  title: selectedCount > 0
2872
3278
  ? (sending ? t('sending') : t('sendSelected', { n: selectedCount }))
2873
3279
  : t('send'),
2874
- 'aria-label': t('send'),
2875
- disabled: selectedCount === 0 || sending,
2876
3280
  onClick: () => sendSegments(selectedActive),
3281
+ icon: h(SendIcon, { size: 14 }),
3282
+ accent: true,
2877
3283
  },
2878
- h(SendIcon, { size: 14 }),
2879
- ),
3284
+ extra: {
3285
+ title: selectedCount > 0 ? t('sendNewSelected', { n: selectedCount }) : t('sendNewSession'),
3286
+ onClick: () => sendNewSession(selectedActive),
3287
+ },
3288
+ }),
2880
3289
  h(
2881
3290
  'button',
2882
3291
  {
@@ -3109,7 +3518,6 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3109
3518
  'aria-label': t('segmentTitle'),
3110
3519
  onClick: () => setTitleEditing(segment.id),
3111
3520
  },
3112
- t('titlePlaceholder'),
3113
3521
  ),
3114
3522
  h(
3115
3523
  'button',
@@ -3122,18 +3530,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3122
3530
  },
3123
3531
  h(ImageIcon, { size: 14 }),
3124
3532
  ),
3125
- h(
3126
- 'button',
3127
- {
3128
- type: 'button',
3129
- className: 'dsh-np-segbtn',
3533
+ actionGroup({
3534
+ size: 'card',
3535
+ disabled: empty,
3536
+ primary: {
3130
3537
  title: t('insertSegment'),
3131
- 'aria-label': t('insertSegment'),
3132
- disabled: empty,
3133
3538
  onClick: () => insertSegments([segment]),
3539
+ icon: h(InsertIcon, { size: 14 }),
3134
3540
  },
3135
- h(InsertIcon, { size: 14 }),
3136
- ),
3541
+ extra: {
3542
+ title: t('insertNewSession'),
3543
+ onClick: () => insertNewSession([segment]),
3544
+ },
3545
+ }),
3137
3546
  h(
3138
3547
  'button',
3139
3548
  {
@@ -3146,18 +3555,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3146
3555
  },
3147
3556
  h(ReplaceIcon, { size: 14 }),
3148
3557
  ),
3149
- h(
3150
- 'button',
3151
- {
3152
- type: 'button',
3153
- className: 'dsh-np-segbtn',
3558
+ actionGroup({
3559
+ size: 'card',
3560
+ disabled: empty || sending,
3561
+ primary: {
3154
3562
  title: t('sendSegment'),
3155
- 'aria-label': t('sendSegment'),
3156
- disabled: empty || sending,
3157
3563
  onClick: () => sendSegments([segment]),
3564
+ icon: h(SendIcon, { size: 14 }),
3158
3565
  },
3159
- h(SendIcon, { size: 14 }),
3160
- ),
3566
+ extra: {
3567
+ title: t('sendNewSession'),
3568
+ onClick: () => sendNewSession([segment]),
3569
+ },
3570
+ }),
3161
3571
  h(
3162
3572
  'button',
3163
3573
  {
@@ -3186,6 +3596,18 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3186
3596
  },
3187
3597
  h(ArchiveIcon, { size: 14 }),
3188
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
+ ),
3189
3611
  ),
3190
3612
  h(
3191
3613
  'div',
@@ -3382,7 +3804,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3382
3804
  h('span', { className: 'dsh-np-count' },
3383
3805
  `${t('chars', { n: totalChars })} · ${t('segments', { n: activeSegments.length })}`
3384
3806
  + (totalImages > 0 ? ` · ${t('images', { n: totalImages })}` : '')),
3385
- h('span', { className: 'dsh-np-hint', title: t('hint') }, t('hint')),
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
+ ),
3386
3818
  );
3387
3819
 
3388
3820
  const toastNode = toast === null
@@ -3422,6 +3854,107 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3422
3854
  onPointerDown: onResizePointerDown,
3423
3855
  });
3424
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
+
3425
3958
  const card = h(
3426
3959
  'section',
3427
3960
  {
@@ -3444,6 +3977,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3444
3977
  },
3445
3978
  header,
3446
3979
  body,
3980
+ previewNode,
3981
+ confirmNode,
3447
3982
  resizeHandle,
3448
3983
  );
3449
3984
 
@@ -3509,6 +4044,43 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
3509
4044
  };
3510
4045
  }, 'dsh-notepad: dictionaries');
3511
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
+
3512
4084
  const slots = ctx.get('slots');
3513
4085
  if (slots === undefined || typeof slots.register !== 'function' || typeof slots.inject !== 'function') {
3514
4086
  console.warn('[dsh-notepad] slots service missing; floating notepad disabled');