dsh-composer-rewind 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +13 -2
  2. package/client.js +429 -12
  3. package/package.json +53 -53
package/README.md CHANGED
@@ -8,6 +8,8 @@ Composer Rewind 是 DeepSeek Harness Web UI 的输入框状态历史插件。它
8
8
 
9
9
  - **完整记录**:输入框里的文字、图片和文件附件、@ 引用、/ 指令,以及当时的 DSH 配置。
10
10
  - **完整恢复**:把一条历史还原成记录时的输入框,包括附件缩略图、引用与指令。
11
+ - **按会话归档**:历史跟着会话走,面板和 `Ctrl+Alt+Z` 只显示当前会话的记录,切换会话自动切换视图。
12
+ - **独立管理页**:在 Harness 设置的“输入框历史”里按会话浏览、删除、清空或导出全部记录。
11
13
  - **不打断输入**:停顿约 450ms 才写入一次,连续打字只更新同一条草稿,不会堆出逐字记录。
12
14
  - **快速找回**:搜索、方向键选择、回车还原;`Ctrl+Alt+Z` 连续回溯更早的输入内容。
13
15
  - **原生外观**:复用 Harness 加号菜单的浮层外壳与主题 token,自动适配深色和浅色模式。
@@ -33,17 +35,26 @@ Composer Rewind 是 DeepSeek Harness Web UI 的输入框状态历史插件。它
33
35
 
34
36
  ## 使用
35
37
 
36
- 点击输入框左侧工具栏的时钟按钮打开历史面板。
38
+ 点击输入框左侧工具栏的时钟按钮打开历史面板。面板只显示**当前会话**的记录。
37
39
 
38
40
  - 顶部搜索框可按文字、附件名或配置筛选。
39
41
  - 记录按“当前草稿 / 今天 / 昨天 / 更早”分组。
40
42
  - `↑` / `↓` 选择,`Enter` 还原,`Esc` 关闭。
41
43
  - 鼠标悬停可查看完整文字、附件与当时的配置;悬浮卡会自动避开屏幕边缘。
42
44
  - 每条历史记录支持还原、复制、删除;“清空”会删除全部历史与附件缓存。
43
- - 在输入框中按 `Ctrl+Alt+Z` 可直接回溯插入上一条,连续按下继续往前。
45
+ - 在输入框中按 `Ctrl+Alt+Z` 可直接回溯插入当前会话的上一条,连续按下继续往前。
44
46
 
45
47
  还原只写入输入框,**不会自动发送**,可以继续修改后再提交。
46
48
 
49
+ ## 设置页
50
+
51
+ Harness 设置中新增“输入框历史”页面,用于跨会话管理记录:
52
+
53
+ - 顶部显示会话数、记录数、附件数与本插件占用的浏览器存储。
54
+ - 记录按会话分组,标出当前会话;每组可单独清空,也可逐条删除。
55
+ - 可一键清空全部记录,或导出 JSON。导出只含文字与元数据,不包含附件文件。
56
+ - 设置页不提供“还原”,需要还原时回到对应会话,用输入框左侧的历史面板。
57
+
47
58
  ## 恢复范围
48
59
 
49
60
  还原一条记录时,插件按以下顺序恢复:
package/client.js CHANGED
@@ -84,6 +84,33 @@ window.__ModuleLoader__.load({
84
84
  failPermission: '权限',
85
85
  failPreset: '使用模式',
86
86
  failSettings: '配置',
87
+ settingsNav: '输入框历史',
88
+ settingsTitle: '输入框历史',
89
+ settingsHint: '按会话查看和管理已保存的输入内容、附件与配置。',
90
+ settingsStatSessions: '会话',
91
+ settingsStatEntries: '记录',
92
+ settingsStatAttachments: '附件',
93
+ settingsStatUsage: '站点存储',
94
+ settingsCurrentSession: '当前会话',
95
+ settingsExport: '导出 JSON',
96
+ settingsExportHint: '只导出文字与元数据,不含附件文件。',
97
+ settingsClearAll: '清空全部',
98
+ settingsClearAllConfirm: '确认清空全部?',
99
+ settingsClearSessionConfirm: '确认清空?',
100
+ settingsClearSession: '清空此会话',
101
+ settingsExpand: '展开全部',
102
+ settingsCollapse: '收起',
103
+ settingsEmpty: '还没有任何会话记录。',
104
+ settingsNoText: '(仅附件)',
105
+ settingsUnknownSession: '未关联会话',
106
+ settingsEntryCount: '条记录',
107
+ settingsAttachmentsUnit: '个附件',
108
+ settingsLastAt: '最后',
109
+ settingsDeleted: '已删除',
110
+ settingsSessionCleared: '该会话记录已清空',
111
+ settingsAllCleared: '全部记录已清空',
112
+ settingsExported: '已开始导出',
113
+ settingsExportEmpty: '没有可导出的记录',
87
114
  },
88
115
  en: {
89
116
  trigger: 'Composer history',
@@ -142,6 +169,33 @@ window.__ModuleLoader__.load({
142
169
  failPermission: 'permission',
143
170
  failPreset: 'mode',
144
171
  failSettings: 'setup',
172
+ settingsNav: 'Composer history',
173
+ settingsTitle: 'Composer history',
174
+ settingsHint: 'Browse and manage the contents, attachments and setup saved per session.',
175
+ settingsStatSessions: 'Sessions',
176
+ settingsStatEntries: 'Entries',
177
+ settingsStatAttachments: 'Attachments',
178
+ settingsStatUsage: 'Site storage',
179
+ settingsCurrentSession: 'Current session',
180
+ settingsExport: 'Export JSON',
181
+ settingsExportHint: 'Text and metadata only; attachment files are not included.',
182
+ settingsClearAll: 'Clear all',
183
+ settingsClearAllConfirm: 'Clear everything?',
184
+ settingsClearSessionConfirm: 'Clear this session?',
185
+ settingsClearSession: 'Clear session',
186
+ settingsExpand: 'Show all',
187
+ settingsCollapse: 'Show less',
188
+ settingsEmpty: 'No session history yet.',
189
+ settingsNoText: '(attachments only)',
190
+ settingsUnknownSession: 'Unlinked session',
191
+ settingsEntryCount: 'entries',
192
+ settingsAttachmentsUnit: 'attachments',
193
+ settingsLastAt: 'last',
194
+ settingsDeleted: 'Deleted',
195
+ settingsSessionCleared: 'Session history cleared',
196
+ settingsAllCleared: 'All history cleared',
197
+ settingsExported: 'Export started',
198
+ settingsExportEmpty: 'Nothing to export',
145
199
  },
146
200
  };
147
201
 
@@ -589,6 +643,11 @@ window.__ModuleLoader__.load({
589
643
  commitEntries([]);
590
644
  }
591
645
 
646
+ function clearSession(sessionId) {
647
+ const key = sessionId || '';
648
+ commitEntries(entries.filter((item) => (item.sessionId || '') !== key));
649
+ }
650
+
592
651
  function patchEntryAttachments(id, info) {
593
652
  if (!Array.isArray(info) || info.length === 0) return;
594
653
  const byKey = new Map(info.map((item) => [item.key, item]));
@@ -1007,10 +1066,27 @@ window.__ModuleLoader__.load({
1007
1066
  let lastState = null;
1008
1067
  let lastCapture = null;
1009
1068
 
1069
+ const sessionListeners = new Set();
1070
+
1071
+ function snapshotSession() {
1072
+ return activeSessionId;
1073
+ }
1074
+
1075
+ function subscribeSession(listener) {
1076
+ sessionListeners.add(listener);
1077
+ return () => sessionListeners.delete(listener);
1078
+ }
1079
+
1080
+ function setActiveSession(sessionId) {
1081
+ if (!sessionId || sessionId === activeSessionId) return;
1082
+ activeSessionId = sessionId;
1083
+ for (const listener of sessionListeners) listener();
1084
+ }
1085
+
1010
1086
  function publishComposer(sessionId, inputActions) {
1011
1087
  if (!sessionId || !inputActions) return;
1012
1088
  composerBySession.set(sessionId, inputActions);
1013
- activeSessionId = sessionId;
1089
+ setActiveSession(sessionId);
1014
1090
  }
1015
1091
 
1016
1092
  function composerActions() {
@@ -1303,6 +1379,10 @@ window.__ModuleLoader__.load({
1303
1379
  return useSyncExternalStore(subscribe, snapshotEntries, snapshotEntries);
1304
1380
  }
1305
1381
 
1382
+ function useActiveSession() {
1383
+ return useSyncExternalStore(subscribeSession, snapshotSession, snapshotSession);
1384
+ }
1385
+
1306
1386
  function useLive() {
1307
1387
  return useSyncExternalStore(subscribe, snapshotLive, snapshotLive);
1308
1388
  }
@@ -1453,6 +1533,61 @@ window.__ModuleLoader__.load({
1453
1533
  return pieces.join(' ').toLowerCase();
1454
1534
  }
1455
1535
 
1536
+ function setupLine(settings) {
1537
+ if (!settings) return '';
1538
+ const parts = [];
1539
+ if (settings.model && settings.model.model) parts.push(settings.model.model);
1540
+ if (settings.permission) parts.push(permissionLabel(settings.permission));
1541
+ if (settings.agentPreset) parts.push(presetLabel(settings.agentPreset));
1542
+ return parts.join(' · ');
1543
+ }
1544
+
1545
+ function sessionTitle(list) {
1546
+ for (const entry of list) {
1547
+ const text = (entry.text || '').replace(/\s+/g, ' ').trim();
1548
+ if (text) return text.length > 30 ? text.slice(0, 30) + '…' : text;
1549
+ }
1550
+ return t('settingsNoText');
1551
+ }
1552
+
1553
+ function sessionGroups(list) {
1554
+ const map = new Map();
1555
+ for (const entry of list) {
1556
+ const id = entry.sessionId || '';
1557
+ if (!map.has(id)) map.set(id, { id, entries: [], at: 0, attachments: 0 });
1558
+ const group = map.get(id);
1559
+ group.entries.push(entry);
1560
+ group.at = Math.max(group.at, entry.at || 0);
1561
+ group.attachments += (entry.attachments || []).length;
1562
+ }
1563
+ return [...map.values()].sort((a, b) => b.at - a.at);
1564
+ }
1565
+
1566
+ function exportHistory() {
1567
+ if (entries.length === 0) {
1568
+ toast(t('settingsExportEmpty'));
1569
+ return;
1570
+ }
1571
+ try {
1572
+ const payload = {
1573
+ plugin: 'dsh-composer-rewind',
1574
+ schema: 3,
1575
+ exportedAt: new Date().toISOString(),
1576
+ entries,
1577
+ };
1578
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
1579
+ const url = URL.createObjectURL(blob);
1580
+ const anchor = document.createElement('a');
1581
+ anchor.href = url;
1582
+ anchor.download = 'composer-rewind-' + new Date().toISOString().slice(0, 10) + '.json';
1583
+ anchor.click();
1584
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
1585
+ toast(t('settingsExported'));
1586
+ } catch {
1587
+ toast(t('copyFailed'));
1588
+ }
1589
+ }
1590
+
1456
1591
  /* ------------------------------------------------------------------ *
1457
1592
  * Styles
1458
1593
  * ------------------------------------------------------------------ */
@@ -1740,6 +1875,68 @@ window.__ModuleLoader__.load({
1740
1875
  line-height:14px!important;color:var(--dsw-alias-label-tertiary)!important;
1741
1876
  background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)!important}
1742
1877
 
1878
+ /* Settings page: follows the harness settings card language. */
1879
+ .pv-set{display:flex;flex-direction:column;gap:16px;width:100%;max-width:720px;margin:0 auto;
1880
+ padding:4px 0 24px;box-sizing:border-box;color:var(--dsw-alias-label-primary);
1881
+ font-size:13px;line-height:1.5;text-align:left}
1882
+ .pv-set *{box-sizing:border-box}
1883
+ .pv-set-head h2{margin:0;font-size:16px;font-weight:600;line-height:1.4}
1884
+ .pv-set-head p{margin:4px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
1885
+ .pv-set-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
1886
+ .pv-set-stat{display:flex;flex-direction:column;gap:2px;min-width:0;padding:10px 12px;
1887
+ border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-lg,12px);
1888
+ background:var(--dsw-alias-bg-layer-1)}
1889
+ .pv-set-stat strong{font-size:16px;font-weight:600;line-height:1.3;font-variant-numeric:tabular-nums}
1890
+ .pv-set-stat span{font-size:11px;line-height:1.4;color:var(--dsw-alias-label-tertiary);
1891
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1892
+ .pv-set-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
1893
+ .pv-set-actions-note{flex:1 1 220px;min-width:0;font-size:11px;color:var(--dsw-alias-label-tertiary)}
1894
+ .pv-set-btn{height:28px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);
1895
+ border-radius:var(--dsw-radius-md,8px);background:transparent;color:var(--dsw-alias-label-primary);
1896
+ font-size:12px;line-height:26px;cursor:pointer;white-space:nowrap}
1897
+ .pv-set-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}
1898
+ .pv-set-btn[data-danger="true"]{color:var(--dsw-alias-state-error-primary)}
1899
+ .pv-set-btn-small{height:24px;padding:0 8px;font-size:11px;line-height:22px}
1900
+ .pv-set-groups{display:flex;flex-direction:column;gap:10px}
1901
+ .pv-set-group{border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-lg,12px);
1902
+ background:var(--dsw-alias-bg-layer-1);overflow:hidden}
1903
+ .pv-set-group-head{display:flex;align-items:center;gap:8px;padding:9px 12px;
1904
+ border-bottom:1px solid var(--dsw-alias-border-l1)}
1905
+ .pv-set-group-title{display:flex;align-items:center;gap:6px;flex:1;min-width:0}
1906
+ .pv-set-group-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
1907
+ font-size:13px;font-weight:500}
1908
+ .pv-set-current{flex:none;padding:0 5px;border-radius:4px;font-size:10px;line-height:16px;
1909
+ color:var(--dsw-alias-brand-primary);
1910
+ background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}
1911
+ .pv-set-group-meta{flex:none;max-width:44%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
1912
+ font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
1913
+ .pv-set-list{list-style:none;margin:0;padding:0}
1914
+ .pv-set-row{display:flex;align-items:flex-start;gap:8px;padding:9px 12px;
1915
+ border-top:1px solid var(--dsw-alias-border-l1)}
1916
+ .pv-set-row:first-child{border-top:0}
1917
+ .pv-set-row:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 4%,transparent)}
1918
+ .pv-set-row-main{flex:1;min-width:0}
1919
+ .pv-set-row-text{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
1920
+ white-space:pre-wrap;overflow-wrap:anywhere;font-size:13px;line-height:1.5}
1921
+ .pv-set-row-meta{display:flex;gap:8px;flex-wrap:wrap;margin-top:3px;font-size:11px;
1922
+ color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
1923
+ .pv-set-remove{flex:none;width:24px;height:24px;padding:0;border:0;border-radius:5px;
1924
+ background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;
1925
+ display:inline-flex;align-items:center;justify-content:center}
1926
+ .pv-set-remove:hover{color:var(--dsw-alias-state-error-primary);
1927
+ background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
1928
+ .pv-set-more{width:100%;height:28px;border:0;border-top:1px solid var(--dsw-alias-border-l1);
1929
+ background:transparent;color:var(--dsw-alias-label-tertiary);font-size:11px;cursor:pointer}
1930
+ .pv-set-more:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
1931
+ .pv-set-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:42px 16px;
1932
+ color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l1);
1933
+ border-radius:var(--dsw-radius-lg,12px);background:var(--dsw-alias-bg-layer-1)}
1934
+ .pv-set-empty p{margin:0;font-size:12px}
1935
+ @media (max-width:560px){
1936
+ .pv-set-stats{grid-template-columns:repeat(2,minmax(0,1fr))}
1937
+ .pv-set-group-meta{display:none}
1938
+ }
1939
+
1743
1940
  @keyframes pv-fade{from{opacity:0}to{opacity:1}}
1744
1941
  @keyframes pv-pop{from{opacity:0;transform:translateY(4px) scale(.994)}to{opacity:1;transform:none}}
1745
1942
  @media (prefers-reduced-motion:reduce){
@@ -2088,7 +2285,6 @@ window.__ModuleLoader__.load({
2088
2285
  stateRef.current = { state, sessionId };
2089
2286
  lastState = { state, sessionId };
2090
2287
  lastDraft = draft;
2091
- activeSessionId = sessionId || activeSessionId;
2092
2288
 
2093
2289
  useEffect(() => {
2094
2290
  if (!sessionId) return;
@@ -2268,22 +2464,33 @@ window.__ModuleLoader__.load({
2268
2464
  * Panel
2269
2465
  * ------------------------------------------------------------------ */
2270
2466
 
2271
- const recall = { index: -1, at: 0, text: '' };
2467
+ const recall = { index: -1, at: 0, text: '', sessionId: null };
2468
+
2469
+ function entriesForSession(sessionId) {
2470
+ if (!sessionId) return entries;
2471
+ return entries.filter((entry) => entry.sessionId === sessionId);
2472
+ }
2272
2473
 
2273
2474
  function recallPrevious() {
2274
- if (entries.length === 0) {
2475
+ const source = entriesForSession(activeSessionId);
2476
+ if (source.length === 0) {
2275
2477
  toast(t('noHistory'));
2276
2478
  return;
2277
2479
  }
2278
2480
  const now = Date.now();
2279
- const sameSession = now - recall.at < 6000 && recall.text !== '' && normalize(currentDraft()) === recall.text;
2280
- const index = sameSession ? Math.min(entries.length - 1, recall.index + 1) : 0;
2281
- const entry = entries[index];
2481
+ const sameSession =
2482
+ now - recall.at < 6000 &&
2483
+ recall.text !== '' &&
2484
+ recall.sessionId === activeSessionId &&
2485
+ normalize(currentDraft()) === recall.text;
2486
+ const index = sameSession ? Math.min(source.length - 1, recall.index + 1) : 0;
2487
+ const entry = source[index];
2282
2488
  if (insertText(entry.text)) {
2283
2489
  recall.index = index;
2284
2490
  recall.at = now;
2285
2491
  recall.text = entry.text;
2286
- toast(t('inserted') + ' ' + (index + 1) + '/' + entries.length);
2492
+ recall.sessionId = activeSessionId;
2493
+ toast(t('inserted') + ' ' + (index + 1) + '/' + source.length);
2287
2494
  } else {
2288
2495
  toast(t('noComposer'));
2289
2496
  }
@@ -2310,6 +2517,7 @@ window.__ModuleLoader__.load({
2310
2517
  const open = useUiOpen();
2311
2518
  const history = useEntries();
2312
2519
  const liveDraft = useLive();
2520
+ const activeSession = useActiveSession();
2313
2521
  const scheme = useScheme();
2314
2522
  const [query, setQuery] = useState('');
2315
2523
  const [activeId, setActiveId] = useState(null);
@@ -2331,11 +2539,13 @@ window.__ModuleLoader__.load({
2331
2539
  : 0;
2332
2540
  const isCurrent =
2333
2541
  !!liveDraft &&
2334
- liveDraft.sessionId === activeSessionId &&
2542
+ liveDraft.sessionId === activeSession &&
2335
2543
  normalize(liveDraft.text) === normalize(currentDraft()) &&
2336
2544
  (liveDraft.attachments || []).length === liveCount;
2337
2545
  const draftRow =
2338
- liveDraft && (liveDraft.text || (liveDraft.attachments || []).length > 0)
2546
+ liveDraft &&
2547
+ (!activeSession || liveDraft.sessionId === activeSession) &&
2548
+ (liveDraft.text || (liveDraft.attachments || []).length > 0)
2339
2549
  ? {
2340
2550
  id: '__live',
2341
2551
  text: liveDraft.text,
@@ -2352,11 +2562,14 @@ window.__ModuleLoader__.load({
2352
2562
  // both "current draft" and the newest history entry. Show it once; the
2353
2563
  // history copy reappears as soon as the composer no longer holds it.
2354
2564
  const liveSignature = draftRow ? entrySignature(draftRow.text, draftRow.attachments) : null;
2355
- const list = history.filter((entry) => !liveSignature || entrySignature(entry.text, entry.attachments) !== liveSignature);
2565
+ // Entries belong to the session they were written in; keep the panel
2566
+ // and Ctrl+Alt+Z scoped to the session that owns the composer now.
2567
+ const scoped = activeSession ? entriesForSession(activeSession) : history;
2568
+ const list = scoped.filter((entry) => !liveSignature || entrySignature(entry.text, entry.attachments) !== liveSignature);
2356
2569
  const matches = needle ? list.filter((entry) => haystack(entry).includes(needle)) : list;
2357
2570
  if (draftRow && (!needle || haystack(draftRow).includes(needle))) return [draftRow, ...matches];
2358
2571
  return matches;
2359
- }, [history, liveDraft, query]);
2572
+ }, [history, liveDraft, query, activeSession]);
2360
2573
 
2361
2574
  const groups = useMemo(() => {
2362
2575
  const order = [];
@@ -2711,6 +2924,195 @@ window.__ModuleLoader__.load({
2711
2924
  return h(React.Fragment, null, panel, h(Toast, { scheme }));
2712
2925
  }
2713
2926
 
2927
+ /* ------------------------------------------------------------------ *
2928
+ * Settings page
2929
+ * ------------------------------------------------------------------ */
2930
+
2931
+ function SettingsPage() {
2932
+ const history = useEntries();
2933
+ const active = useActiveSession();
2934
+ const [confirmAll, setConfirmAll] = useState(false);
2935
+ const [confirmGroup, setConfirmGroup] = useState(null);
2936
+ const [expanded, setExpanded] = useState({});
2937
+ const [usage, setUsage] = useState(null);
2938
+
2939
+ useEffect(() => {
2940
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
2941
+ if (!nav || !nav.storage || typeof nav.storage.estimate !== 'function') return undefined;
2942
+ let alive = true;
2943
+ nav.storage.estimate().then(
2944
+ (value) => {
2945
+ if (alive) setUsage(value && typeof value.usage === 'number' ? value.usage : null);
2946
+ },
2947
+ () => {},
2948
+ );
2949
+ return () => {
2950
+ alive = false;
2951
+ };
2952
+ }, [history]);
2953
+
2954
+ const groups = useMemo(() => sessionGroups(history), [history]);
2955
+ const attachmentCount = history.reduce((sum, entry) => sum + (entry.attachments || []).length, 0);
2956
+
2957
+ const clearGroup = (id) => {
2958
+ if (confirmGroup !== id) {
2959
+ setConfirmGroup(id);
2960
+ return;
2961
+ }
2962
+ clearSession(id);
2963
+ setConfirmGroup(null);
2964
+ setExpanded((value) => ({ ...value, [id]: false }));
2965
+ toast(t('settingsSessionCleared'));
2966
+ };
2967
+
2968
+ const clearEverything = () => {
2969
+ if (!confirmAll) {
2970
+ setConfirmAll(true);
2971
+ return;
2972
+ }
2973
+ clearAll();
2974
+ setConfirmAll(false);
2975
+ setExpanded({});
2976
+ toast(t('settingsAllCleared'));
2977
+ };
2978
+
2979
+ const stat = (label, value) =>
2980
+ h(
2981
+ 'div',
2982
+ { className: 'pv-set-stat', key: label },
2983
+ h('strong', null, value),
2984
+ h('span', null, label),
2985
+ );
2986
+
2987
+ return h(
2988
+ 'div',
2989
+ { className: 'pv-set' },
2990
+ h(
2991
+ 'header',
2992
+ { className: 'pv-set-head' },
2993
+ h('h2', null, t('settingsTitle')),
2994
+ h('p', null, t('settingsHint')),
2995
+ ),
2996
+ h(
2997
+ 'div',
2998
+ { className: 'pv-set-stats' },
2999
+ stat(t('settingsStatSessions'), String(groups.length)),
3000
+ stat(t('settingsStatEntries'), String(history.length)),
3001
+ stat(t('settingsStatAttachments'), String(attachmentCount)),
3002
+ stat(t('settingsStatUsage'), usage == null ? '—' : usage === 0 ? '0 B' : formatBytes(usage)),
3003
+ ),
3004
+ h(
3005
+ 'div',
3006
+ { className: 'pv-set-actions' },
3007
+ h('span', { className: 'pv-set-actions-note' }, t('settingsExportHint')),
3008
+ h(
3009
+ 'button',
3010
+ { type: 'button', className: 'pv-set-btn', onClick: exportHistory },
3011
+ t('settingsExport'),
3012
+ ),
3013
+ h(
3014
+ 'button',
3015
+ {
3016
+ type: 'button',
3017
+ className: 'pv-set-btn',
3018
+ 'data-danger': 'true',
3019
+ onClick: clearEverything,
3020
+ },
3021
+ confirmAll ? t('settingsClearAllConfirm') : t('settingsClearAll'),
3022
+ ),
3023
+ ),
3024
+ groups.length === 0
3025
+ ? h('div', { className: 'pv-set-empty' }, h(Icon, { paths: ACTION_PATHS.search, size: 20 }), h('p', null, t('settingsEmpty')))
3026
+ : h(
3027
+ 'div',
3028
+ { className: 'pv-set-groups' },
3029
+ groups.map((group) => {
3030
+ const openGroup = !!expanded[group.id];
3031
+ const shown = openGroup ? group.entries : group.entries.slice(0, 6);
3032
+ return h(
3033
+ 'section',
3034
+ { className: 'pv-set-group', key: group.id || 'unknown' },
3035
+ h(
3036
+ 'div',
3037
+ { className: 'pv-set-group-head' },
3038
+ h(
3039
+ 'div',
3040
+ { className: 'pv-set-group-title' },
3041
+ h('span', { className: 'pv-set-group-name' }, group.id ? sessionTitle(group.entries) : t('settingsUnknownSession')),
3042
+ group.id === active && group.id ? h('span', { className: 'pv-set-current' }, t('settingsCurrentSession')) : null,
3043
+ ),
3044
+ h(
3045
+ 'span',
3046
+ { className: 'pv-set-group-meta' },
3047
+ group.entries.length + ' ' + t('settingsEntryCount') +
3048
+ (group.attachments ? ' · ' + group.attachments + ' ' + t('settingsAttachmentsUnit') : '') +
3049
+ ' · ' + t('settingsLastAt') + ' ' + formatTime(group.at) +
3050
+ (group.id ? ' · #' + group.id.slice(0, 8) : ''),
3051
+ ),
3052
+ h(
3053
+ 'button',
3054
+ {
3055
+ type: 'button',
3056
+ className: 'pv-set-btn pv-set-btn-small',
3057
+ onClick: () => clearGroup(group.id),
3058
+ },
3059
+ confirmGroup === group.id ? t('settingsClearSessionConfirm') : t('settingsClearSession'),
3060
+ ),
3061
+ ),
3062
+ h(
3063
+ 'ul',
3064
+ { className: 'pv-set-list' },
3065
+ shown.map((entry) =>
3066
+ h(
3067
+ 'li',
3068
+ { className: 'pv-set-row', key: entry.id },
3069
+ h(
3070
+ 'div',
3071
+ { className: 'pv-set-row-main' },
3072
+ h('div', { className: 'pv-set-row-text' }, entry.text || t('settingsNoText')),
3073
+ h(
3074
+ 'div',
3075
+ { className: 'pv-set-row-meta' },
3076
+ h('span', null, formatTime(entry.at)),
3077
+ entry.attachments && entry.attachments.length
3078
+ ? h('span', null, entry.attachments.length + ' ' + t('settingsAttachmentsUnit'))
3079
+ : null,
3080
+ setupLine(entry.settings) ? h('span', null, setupLine(entry.settings)) : null,
3081
+ ),
3082
+ ),
3083
+ h(
3084
+ 'button',
3085
+ {
3086
+ type: 'button',
3087
+ className: 'pv-set-remove',
3088
+ 'aria-label': t('remove'),
3089
+ onClick: () => {
3090
+ removeEntry(entry.id);
3091
+ toast(t('settingsDeleted'));
3092
+ },
3093
+ },
3094
+ h(Icon, { paths: ACTION_PATHS.trash, size: 14 }),
3095
+ ),
3096
+ ),
3097
+ ),
3098
+ ),
3099
+ group.entries.length > 6
3100
+ ? h(
3101
+ 'button',
3102
+ {
3103
+ type: 'button',
3104
+ className: 'pv-set-more',
3105
+ onClick: () => setExpanded((value) => ({ ...value, [group.id]: !openGroup })),
3106
+ },
3107
+ openGroup ? t('settingsCollapse') : t('settingsExpand'),
3108
+ )
3109
+ : null,
3110
+ );
3111
+ }),
3112
+ ),
3113
+ );
3114
+ }
3115
+
2714
3116
  function Toast(props) {
2715
3117
  const message = useToast();
2716
3118
  if (!message) return null;
@@ -2730,8 +3132,12 @@ window.__ModuleLoader__.load({
2730
3132
  setLive,
2731
3133
  setLiveSnapshot,
2732
3134
  clearAll,
3135
+ clearSession,
2733
3136
  removeEntry,
2734
3137
  publishComposer,
3138
+ setActiveSession,
3139
+ activeSession: () => activeSessionId,
3140
+ entriesForSession,
2735
3141
  insertText,
2736
3142
  recallPrevious,
2737
3143
  currentDraft,
@@ -2770,6 +3176,17 @@ window.__ModuleLoader__.load({
2770
3176
  ctx.slots.inject('shell.overlay', () =>
2771
3177
  ctx.slots.register({ name: 'shell.overlay', id: 'composer-rewind-panel', order: 90 }, Panel),
2772
3178
  );
3179
+ ctx.slots.inject('settings.section', () =>
3180
+ ctx.slots.register(
3181
+ {
3182
+ name: 'settings.section',
3183
+ id: 'composer-rewind-settings',
3184
+ order: 30,
3185
+ label: () => t('settingsNav'),
3186
+ },
3187
+ SettingsPage,
3188
+ ),
3189
+ );
2773
3190
  // Release object URLs when the page goes away.
2774
3191
  if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
2775
3192
  window.addEventListener('pagehide', () => {
package/package.json CHANGED
@@ -1,53 +1,53 @@
1
- {
2
- "name": "dsh-composer-rewind",
3
- "version": "2.0.4",
4
- "type": "module",
5
- "description": "Composer input history for the DeepSeek Harness Web UI: records composer contents, attachments, command tokens and the active DSH setup, then restores the whole composer state.",
6
- "keywords": [
7
- "dsh",
8
- "dsh-plugin",
9
- "deepseek-harness",
10
- "composer-history",
11
- "composer"
12
- ],
13
- "repository": {
14
- "type": "git",
15
- "url": "git+https://github.com/QIUZAIYOU/dsh-composer-rewind.git"
16
- },
17
- "homepage": "https://github.com/QIUZAIYOU/dsh-composer-rewind#readme",
18
- "bugs": {
19
- "url": "https://github.com/QIUZAIYOU/dsh-composer-rewind/issues"
20
- },
21
- "publishConfig": {
22
- "access": "public"
23
- },
24
- "exports": {
25
- ".": "./index.js",
26
- "./client": "./client.js",
27
- "./locale/*.json": "./locale/*.json",
28
- "./icon": "./icon.svg"
29
- },
30
- "files": [
31
- "index.js",
32
- "client.js",
33
- "cordis.patch.yml",
34
- "locale/*.json",
35
- "icon.svg",
36
- "README.md"
37
- ],
38
- "dsh": {
39
- "bundle": {
40
- "patch": "./cordis.patch.yml"
41
- },
42
- "client": {
43
- "platform": "web",
44
- "immediately": true,
45
- "external": [
46
- "@deepseek-ai/dsh-client-ui-primitives"
47
- ],
48
- "inject": [
49
- "@deepseek-ai/dsh-client-ui-conversation"
50
- ]
51
- }
52
- }
53
- }
1
+ {
2
+ "name": "dsh-composer-rewind",
3
+ "version": "2.1.0",
4
+ "type": "module",
5
+ "description": "Per-session composer input history for the DeepSeek Harness Web UI: records composer contents, attachments, command tokens and the active DSH setup, restores the whole composer state, and manages history per session from a settings page.",
6
+ "keywords": [
7
+ "dsh",
8
+ "dsh-plugin",
9
+ "deepseek-harness",
10
+ "composer-history",
11
+ "composer"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/QIUZAIYOU/dsh-composer-rewind.git"
16
+ },
17
+ "homepage": "https://github.com/QIUZAIYOU/dsh-composer-rewind#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/QIUZAIYOU/dsh-composer-rewind/issues"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "exports": {
25
+ ".": "./index.js",
26
+ "./client": "./client.js",
27
+ "./locale/*.json": "./locale/*.json",
28
+ "./icon": "./icon.svg"
29
+ },
30
+ "files": [
31
+ "index.js",
32
+ "client.js",
33
+ "cordis.patch.yml",
34
+ "locale/*.json",
35
+ "icon.svg",
36
+ "README.md"
37
+ ],
38
+ "dsh": {
39
+ "bundle": {
40
+ "patch": "./cordis.patch.yml"
41
+ },
42
+ "client": {
43
+ "platform": "web",
44
+ "immediately": true,
45
+ "external": [
46
+ "@deepseek-ai/dsh-client-ui-primitives"
47
+ ],
48
+ "inject": [
49
+ "@deepseek-ai/dsh-client-ui-conversation"
50
+ ]
51
+ }
52
+ }
53
+ }