dsh-composer-rewind 2.0.3 → 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.
- package/README.md +13 -2
- package/client.js +456 -22
- package/package.json +2 -2
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
|
-
|
|
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;
|
|
@@ -2196,16 +2392,32 @@ window.__ModuleLoader__.load({
|
|
|
2196
2392
|
let panelFrame = null;
|
|
2197
2393
|
|
|
2198
2394
|
function measurePanelFrame() {
|
|
2199
|
-
if (typeof window === 'undefined') return { left: 12, width: 360, bottom: 96, maxHeight:
|
|
2395
|
+
if (typeof window === 'undefined') return { left: 12, width: 360, bottom: 96, maxHeight: 400 };
|
|
2200
2396
|
const node = typeof document !== 'undefined' ? document.querySelector('.pv-trigger') : null;
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
const
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2397
|
+
// The `+` menu is anchored to the composer card, not to the button:
|
|
2398
|
+
// same left edge, same width, 4px above the card.
|
|
2399
|
+
const card = node && node.closest ? node.closest('[data-composer-card]') : null;
|
|
2400
|
+
const rim = card || node;
|
|
2401
|
+
const rect = rim && rim.getBoundingClientRect ? rim.getBoundingClientRect() : null;
|
|
2402
|
+
if (!rect) return { left: 12, width: Math.min(360, window.innerWidth - 24), bottom: 96, maxHeight: 400 };
|
|
2403
|
+
const width = Math.max(240, Math.min(rect.width, window.innerWidth - 24));
|
|
2404
|
+
const left = Math.max(12, Math.min(rect.left, window.innerWidth - width - 12));
|
|
2405
|
+
const bottom = Math.max(4, window.innerHeight - rect.top + 4);
|
|
2406
|
+
// MenuView uses a 400px cap and keeps 84px of viewport above itself.
|
|
2407
|
+
const above = rect.top - 84;
|
|
2408
|
+
return { left, width, bottom, maxHeight: Math.max(160, Math.min(400, above)) };
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* The host `+` menu treats pointerdowns inside the composer card as its
|
|
2413
|
+
* own and stays open. Toggle it closed before we show the history panel,
|
|
2414
|
+
* otherwise both surfaces occupy the exact same frame.
|
|
2415
|
+
*/
|
|
2416
|
+
function dismissHostMenu() {
|
|
2417
|
+
if (typeof document === 'undefined') return;
|
|
2418
|
+
const menu = document.querySelector('[data-trigger-menu]');
|
|
2419
|
+
if (!menu || typeof document.body.dispatchEvent !== 'function') return;
|
|
2420
|
+
document.body.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, cancelable: true }));
|
|
2209
2421
|
}
|
|
2210
2422
|
|
|
2211
2423
|
function placeHoverCard(rect, width, height) {
|
|
@@ -2240,7 +2452,8 @@ window.__ModuleLoader__.load({
|
|
|
2240
2452
|
onClick: () => {
|
|
2241
2453
|
publishComposer(props.sessionId, props.inputActions);
|
|
2242
2454
|
panelFrame = open ? null : measurePanelFrame();
|
|
2243
|
-
|
|
2455
|
+
if (!open) dismissHostMenu();
|
|
2456
|
+
setOpen(!open);
|
|
2244
2457
|
},
|
|
2245
2458
|
},
|
|
2246
2459
|
h(TriggerIcon, { size: 18 }),
|
|
@@ -2251,22 +2464,33 @@ window.__ModuleLoader__.load({
|
|
|
2251
2464
|
* Panel
|
|
2252
2465
|
* ------------------------------------------------------------------ */
|
|
2253
2466
|
|
|
2254
|
-
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
|
+
}
|
|
2255
2473
|
|
|
2256
2474
|
function recallPrevious() {
|
|
2257
|
-
|
|
2475
|
+
const source = entriesForSession(activeSessionId);
|
|
2476
|
+
if (source.length === 0) {
|
|
2258
2477
|
toast(t('noHistory'));
|
|
2259
2478
|
return;
|
|
2260
2479
|
}
|
|
2261
2480
|
const now = Date.now();
|
|
2262
|
-
const sameSession =
|
|
2263
|
-
|
|
2264
|
-
|
|
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];
|
|
2265
2488
|
if (insertText(entry.text)) {
|
|
2266
2489
|
recall.index = index;
|
|
2267
2490
|
recall.at = now;
|
|
2268
2491
|
recall.text = entry.text;
|
|
2269
|
-
|
|
2492
|
+
recall.sessionId = activeSessionId;
|
|
2493
|
+
toast(t('inserted') + ' ' + (index + 1) + '/' + source.length);
|
|
2270
2494
|
} else {
|
|
2271
2495
|
toast(t('noComposer'));
|
|
2272
2496
|
}
|
|
@@ -2293,6 +2517,7 @@ window.__ModuleLoader__.load({
|
|
|
2293
2517
|
const open = useUiOpen();
|
|
2294
2518
|
const history = useEntries();
|
|
2295
2519
|
const liveDraft = useLive();
|
|
2520
|
+
const activeSession = useActiveSession();
|
|
2296
2521
|
const scheme = useScheme();
|
|
2297
2522
|
const [query, setQuery] = useState('');
|
|
2298
2523
|
const [activeId, setActiveId] = useState(null);
|
|
@@ -2314,11 +2539,13 @@ window.__ModuleLoader__.load({
|
|
|
2314
2539
|
: 0;
|
|
2315
2540
|
const isCurrent =
|
|
2316
2541
|
!!liveDraft &&
|
|
2317
|
-
liveDraft.sessionId ===
|
|
2542
|
+
liveDraft.sessionId === activeSession &&
|
|
2318
2543
|
normalize(liveDraft.text) === normalize(currentDraft()) &&
|
|
2319
2544
|
(liveDraft.attachments || []).length === liveCount;
|
|
2320
2545
|
const draftRow =
|
|
2321
|
-
liveDraft &&
|
|
2546
|
+
liveDraft &&
|
|
2547
|
+
(!activeSession || liveDraft.sessionId === activeSession) &&
|
|
2548
|
+
(liveDraft.text || (liveDraft.attachments || []).length > 0)
|
|
2322
2549
|
? {
|
|
2323
2550
|
id: '__live',
|
|
2324
2551
|
text: liveDraft.text,
|
|
@@ -2335,11 +2562,14 @@ window.__ModuleLoader__.load({
|
|
|
2335
2562
|
// both "current draft" and the newest history entry. Show it once; the
|
|
2336
2563
|
// history copy reappears as soon as the composer no longer holds it.
|
|
2337
2564
|
const liveSignature = draftRow ? entrySignature(draftRow.text, draftRow.attachments) : null;
|
|
2338
|
-
|
|
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);
|
|
2339
2569
|
const matches = needle ? list.filter((entry) => haystack(entry).includes(needle)) : list;
|
|
2340
2570
|
if (draftRow && (!needle || haystack(draftRow).includes(needle))) return [draftRow, ...matches];
|
|
2341
2571
|
return matches;
|
|
2342
|
-
}, [history, liveDraft, query]);
|
|
2572
|
+
}, [history, liveDraft, query, activeSession]);
|
|
2343
2573
|
|
|
2344
2574
|
const groups = useMemo(() => {
|
|
2345
2575
|
const order = [];
|
|
@@ -2694,6 +2924,195 @@ window.__ModuleLoader__.load({
|
|
|
2694
2924
|
return h(React.Fragment, null, panel, h(Toast, { scheme }));
|
|
2695
2925
|
}
|
|
2696
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
|
+
|
|
2697
3116
|
function Toast(props) {
|
|
2698
3117
|
const message = useToast();
|
|
2699
3118
|
if (!message) return null;
|
|
@@ -2713,8 +3132,12 @@ window.__ModuleLoader__.load({
|
|
|
2713
3132
|
setLive,
|
|
2714
3133
|
setLiveSnapshot,
|
|
2715
3134
|
clearAll,
|
|
3135
|
+
clearSession,
|
|
2716
3136
|
removeEntry,
|
|
2717
3137
|
publishComposer,
|
|
3138
|
+
setActiveSession,
|
|
3139
|
+
activeSession: () => activeSessionId,
|
|
3140
|
+
entriesForSession,
|
|
2718
3141
|
insertText,
|
|
2719
3142
|
recallPrevious,
|
|
2720
3143
|
currentDraft,
|
|
@@ -2753,6 +3176,17 @@ window.__ModuleLoader__.load({
|
|
|
2753
3176
|
ctx.slots.inject('shell.overlay', () =>
|
|
2754
3177
|
ctx.slots.register({ name: 'shell.overlay', id: 'composer-rewind-panel', order: 90 }, Panel),
|
|
2755
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
|
+
);
|
|
2756
3190
|
// Release object URLs when the page goes away.
|
|
2757
3191
|
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
2758
3192
|
window.addEventListener('pagehide', () => {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-composer-rewind",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
|
8
8
|
"dsh-plugin",
|