dsh-project-room 0.2.0 → 0.2.1
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 +101 -34
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -819,23 +819,40 @@ function summarizeMessage(raw) {
|
|
|
819
819
|
return () => { cancelled = true; clearInterval(timer); };
|
|
820
820
|
|
|
821
821
|
}, []);
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
822
|
+
// ── 幽灵会话「事后隐藏」(2026-11-10 用户令:白名单改事后隐藏)──────────────
|
|
823
|
+
// 背景:曾经用「磁盘活会话白名单」全量过滤(每 120s 拉 /project-room/alive,
|
|
824
|
+
// 不在白名单里的会话直接不显示)。它确实能藏住「前端快照有、磁盘目录没了」的幽灵
|
|
825
|
+
// (归档必报 no such session,DSH 又没有删除 API),但代价是**新建会话要等下一次
|
|
826
|
+
// 轮询才认识** —— 分诊分身只活 30 秒,正好卡在两次轮询之间,侧栏从来没显示过它。
|
|
827
|
+
//
|
|
828
|
+
// 用户 2026-11-10 明确选了「事后隐藏」:默认**不过滤任何会话**,只有真的点归档且
|
|
829
|
+
// 失败时,才把那个 id 记进 localStorage 的隐藏集,下次渲染跳过它。
|
|
830
|
+
// 好处:新会话零延迟显示;幽灵删一次之后就消失。
|
|
831
|
+
const HIDDEN_KEY = 'project-room:hidden-sessions';
|
|
832
|
+
const bareId = (v) => String(v || '').replace(/^session-/, '').toLowerCase();
|
|
833
|
+
const [hiddenIds, setHiddenIds] = React.useState(() => {
|
|
834
|
+
try {
|
|
835
|
+
const raw = window.localStorage.getItem(HIDDEN_KEY);
|
|
836
|
+
const arr = raw ? JSON.parse(raw) : [];
|
|
837
|
+
return new Set(Array.isArray(arr) ? arr.map(bareId) : []);
|
|
838
|
+
} catch (error) { return new Set(); }
|
|
839
|
+
});
|
|
840
|
+
const hideSession = React.useCallback((id) => {
|
|
841
|
+
const bare = bareId(id);
|
|
842
|
+
if (!bare) return;
|
|
843
|
+
setHiddenIds((prev) => {
|
|
844
|
+
if (prev.has(bare)) return prev;
|
|
845
|
+
const next = new Set(prev);
|
|
846
|
+
next.add(bare);
|
|
828
847
|
try {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const timer = setInterval(load, 120000);
|
|
838
|
-
return () => { cancelled = true; clearInterval(timer); };
|
|
848
|
+
window.localStorage.setItem(HIDDEN_KEY, JSON.stringify([...next]));
|
|
849
|
+
} catch (error) { /* 存不下也不影响本次隐藏 */ }
|
|
850
|
+
return next;
|
|
851
|
+
});
|
|
852
|
+
}, []);
|
|
853
|
+
const unhideAll = React.useCallback(() => {
|
|
854
|
+
setHiddenIds(new Set());
|
|
855
|
+
try { window.localStorage.removeItem(HIDDEN_KEY); } catch (error) { /* ignore */ }
|
|
839
856
|
}, []);
|
|
840
857
|
|
|
841
858
|
// 1s 心跳:驱动「今日运行时长」与相对时间
|
|
@@ -853,10 +870,9 @@ function summarizeMessage(raw) {
|
|
|
853
870
|
for (const [id, session] of Object.entries(byId)) {
|
|
854
871
|
if (!session || session.blank) continue;
|
|
855
872
|
if (archived.has(String(id))) continue;
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
if (aliveIds && !aliveIds.has(String(id).replace(/^session-/, '').toLowerCase())) continue;
|
|
873
|
+
// 2026-11-10(用户令):白名单改「事后隐藏」——只跳过**归档失败过**的会话,
|
|
874
|
+
// 不再用磁盘白名单全量过滤(那样会让新建会话因轮询延迟而整段生命期不可见)。
|
|
875
|
+
if (hiddenIds.has(bareId(id))) continue;
|
|
860
876
|
const parsed = projectOfTitle(session.displayTitle || session.title);
|
|
861
877
|
const project = parsed ? parsed.project : LOOSE_GROUP;
|
|
862
878
|
const member = parsed ? parsed.member : (session.displayTitle || session.title || String(id).slice(-6));
|
|
@@ -878,7 +894,7 @@ function summarizeMessage(raw) {
|
|
|
878
894
|
});
|
|
879
895
|
}
|
|
880
896
|
return map;
|
|
881
|
-
}, [byId, archived,
|
|
897
|
+
}, [byId, archived, hiddenIds]);
|
|
882
898
|
|
|
883
899
|
// 空闲时长补全(2026-11-09):对还没有 lastActive 的会话拉 /hub/session-events?limit=1
|
|
884
900
|
// 取最后一条事件的 time。分身(标题带|)优先——用户最常盯的就是分身行。
|
|
@@ -999,10 +1015,9 @@ function summarizeMessage(raw) {
|
|
|
999
1015
|
// 「未分组」= 标题里没有分隔符的会话(历史测试/一次性对话)。
|
|
1000
1016
|
// 默认不显示,避免一大坨散落会话把项目列表挤下去;需要时点头部按钮展开。
|
|
1001
1017
|
const looseAll = groups.get(LOOSE_GROUP) || [];
|
|
1002
|
-
//
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
: looseAll;
|
|
1018
|
+
// 2026-11-10(用户令):白名单改「事后隐藏」后,groups 里已经按 hiddenIds 过滤过了,
|
|
1019
|
+
// 这里不再二次过滤(否则又回到「新建会话要等白名单」的老问题)。
|
|
1020
|
+
const looseMembers = looseAll;
|
|
1006
1021
|
const shownProjects = React.useMemo(
|
|
1007
1022
|
() => projectNames.filter((name) => showLoose || name !== LOOSE_GROUP),
|
|
1008
1023
|
[projectNames, showLoose],
|
|
@@ -1090,11 +1105,25 @@ function summarizeMessage(raw) {
|
|
|
1090
1105
|
const bodyIds = group.members.filter((m) => !m.isClone).map((m) => String(m.id));
|
|
1091
1106
|
const isCurrent = Boolean(cursorSessionId) && bodyIds.includes(String(cursorSessionId));
|
|
1092
1107
|
const done = group.mode === 'done';
|
|
1093
|
-
|
|
1094
|
-
|
|
1108
|
+
// 2026-11-11 修「本体没有运行、快捷栏还在显示计时,而侧边栏正常」(用户截图):
|
|
1109
|
+
// 这个 pill 代表的是**本体**——名字取 group.name(本体名)、点击开本体、data-session-ids
|
|
1110
|
+
// 也只有本体。所以绿点与已运行时长必须来自**本体自己的 running**。
|
|
1111
|
+
// 原实现取 `group.running` 里最近活跃的那个成员:分身还在跑时,那一项就是分身,
|
|
1112
|
+
// 于是分身的已运行时长被画到本体 pill 上(截图里 pill 的 2m29s 与分身子行的 2m29s
|
|
1113
|
+
// 完全相等,而侧栏本体芯片没有时长 = 本体当时是空闲的)。
|
|
1114
|
+
// 侧栏一直是对的:本体芯片按 m.running 判、分身行按 c.running 判,两条各算各的。
|
|
1115
|
+
// 分身的运行状态由下面 .pr-run-clones 子行自己显示,信息不丢。
|
|
1116
|
+
// 孤儿分身组(本体已归档/改名,group.name 就是分身名)没有本体可参照 → 仍用组内运行成员。
|
|
1117
|
+
const bodyMemberOfGroup = (() => {
|
|
1118
|
+
const list = (groups && typeof groups.get === 'function') ? (groups.get(group.project) || []) : [];
|
|
1119
|
+
return list.find((m) => !m.isClone && m.name === group.name) || null;
|
|
1120
|
+
})();
|
|
1121
|
+
const runMember = (bodyMemberOfGroup ? [bodyMemberOfGroup] : (group.running.length ? group.running : group.members))
|
|
1122
|
+
.filter((m) => m && m.running)
|
|
1095
1123
|
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))[0];
|
|
1124
|
+
const bodyRunning = Boolean(runMember);
|
|
1096
1125
|
const startedAt = runMember ? runStartRef.current.get(runMember.id) : undefined;
|
|
1097
|
-
const elapsed = !done &&
|
|
1126
|
+
const elapsed = !done && bodyRunning && startedAt ? formatDuration(tick - startedAt) : '';
|
|
1098
1127
|
const emoji = avatars && typeof avatars[group.name] === 'string' ? avatars[group.name].trim() : '';
|
|
1099
1128
|
const hue = hashHue(group.project || group.name);
|
|
1100
1129
|
return h('div', { className: 'pr-run-wrap', key: 'wrap-' + group.key }, h('button', {
|
|
@@ -1106,7 +1135,9 @@ function summarizeMessage(raw) {
|
|
|
1106
1135
|
// 原来放全组成员 → 点分身时 pill(代表本体)也一起高亮。
|
|
1107
1136
|
'data-session-ids': group.members.filter((mb) => !mb.isClone).map((mb) => String(mb.id)).join(','),
|
|
1108
1137
|
title: `${group.project ? group.project + ' · ' : ''}${group.name}`
|
|
1109
|
-
|
|
1138
|
+
// 2026-11-11:不再对空闲的本体谎称「运行中」——本体没跑时 pill 只报「分身运行中」。
|
|
1139
|
+
+ (done ? ' · 完成待查看'
|
|
1140
|
+
: (bodyRunning ? (elapsed ? ` · 运行中 ${elapsed}` : ' · 运行中') : ' · 本体空闲(分身运行中)'))
|
|
1110
1141
|
+ (group.extra ? `(含 ${group.extra} 个分身)` : ''),
|
|
1111
1142
|
onClick: (event) => {
|
|
1112
1143
|
event.stopPropagation();
|
|
@@ -1153,7 +1184,9 @@ function summarizeMessage(raw) {
|
|
|
1153
1184
|
group.extra > 0 ? h('span', { className: 'pr-run-badge', title: `${group.extra} 个分身` }, `+${group.extra}`) : null,
|
|
1154
1185
|
done
|
|
1155
1186
|
? h('span', { className: 'pr-run-done', title: '已完成,点开查看' }, h('span', { className: 'pr-run-check' }, '✓'), '完成')
|
|
1156
|
-
|
|
1187
|
+
// 2026-11-11:绿点也只跟本体走——本体空闲、只剩分身在跑时不给本体挂绿点
|
|
1188
|
+
// (分身子行自己有 pr-clone-dot.on,不丢状态)。
|
|
1189
|
+
: (bodyRunning ? h('span', { className: 'pr-running-dot', title: '运行中' }) : null),
|
|
1157
1190
|
elapsed ? h('span', { className: 'pr-run-elapsed' }, elapsed) : null,
|
|
1158
1191
|
),
|
|
1159
1192
|
group.clones && group.clones.length
|
|
@@ -1217,8 +1250,12 @@ function summarizeMessage(raw) {
|
|
|
1217
1250
|
});
|
|
1218
1251
|
const data = await res.json();
|
|
1219
1252
|
if (data && data.ok) {
|
|
1220
|
-
|
|
1221
|
-
|
|
1253
|
+
// 2026-11-10(用户令「事后隐藏」):归档失败 = 磁盘上根本没这个会话(幽灵)。
|
|
1254
|
+
// 既然删不掉,就记进隐藏集,下次渲染跳过它。
|
|
1255
|
+
const failedList = Array.isArray(data.failed) ? data.failed : [];
|
|
1256
|
+
for (const f of failedList) hideSession(f && f.id);
|
|
1257
|
+
const failNote = failedList.length
|
|
1258
|
+
? `,失败 ${failedList.length}(${String((failedList[0] && failedList[0].error) || '').slice(0, 60)};已隐藏)`
|
|
1222
1259
|
: '';
|
|
1223
1260
|
setClearMsg(`已归档 ${data.archived}/${data.total}${failNote}`);
|
|
1224
1261
|
console.log('[project-room] archive result', data);
|
|
@@ -1260,8 +1297,20 @@ function summarizeMessage(raw) {
|
|
|
1260
1297
|
body: JSON.stringify({ sessionIds: [menu.id] }),
|
|
1261
1298
|
});
|
|
1262
1299
|
const data = await res.json();
|
|
1263
|
-
|
|
1264
|
-
|
|
1300
|
+
const okArchived = Boolean(data && data.ok && data.archived);
|
|
1301
|
+
if (okArchived) {
|
|
1302
|
+
setMenuMsg('已归档');
|
|
1303
|
+
} else {
|
|
1304
|
+
// 2026-11-10(用户令「事后隐藏」):单个归档失败 = 幽灵会话,记进隐藏集。
|
|
1305
|
+
hideSession(menu.id);
|
|
1306
|
+
const reason = (data && data.failed && data.failed[0] && data.failed[0].error)
|
|
1307
|
+
|| (data && data.error) || '未知';
|
|
1308
|
+
setMenuMsg(`失败:${reason}(已隐藏)`.slice(0, 90));
|
|
1309
|
+
}
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
hideSession(menu.id);
|
|
1312
|
+
setMenuMsg(`失败:${String((error && error.message) || error)}(已隐藏)`.slice(0, 90));
|
|
1313
|
+
}
|
|
1265
1314
|
},
|
|
1266
1315
|
}, '归档这个会话'),
|
|
1267
1316
|
h('button', { className: 'pr-menu-item', onClick: () => { copy(menu.id, ' sessionId'); } }, '复制 sessionId'),
|
|
@@ -1296,6 +1345,24 @@ function summarizeMessage(raw) {
|
|
|
1296
1345
|
'注意:这只影响本插件的排序与默认名。若要让「管理者协议注入」也认这个名字,'
|
|
1297
1346
|
+ '还需要改 hub-desktop-ui 里的 MANAGER_NAMES(那是另一个插件的硬编码)。'),
|
|
1298
1347
|
settings.msg ? h('div', { className: 'pr-err' }, settings.msg) : null,
|
|
1348
|
+
// 2026-11-10(用户令「事后隐藏」):归档失败过的会话被记进 localStorage 隐藏集,
|
|
1349
|
+
// 这里提供恢复入口,免得误隐藏之后找不回来。
|
|
1350
|
+
h('div', { className: 'pr-hint', style: { marginTop: '10px' } },
|
|
1351
|
+
`已隐藏的会话:${hiddenIds.size} 个`),
|
|
1352
|
+
h('button', {
|
|
1353
|
+
type: 'button',
|
|
1354
|
+
disabled: hiddenIds.size === 0,
|
|
1355
|
+
style: {
|
|
1356
|
+
marginTop: '4px', padding: '4px 10px', borderRadius: '6px', fontSize: '11px',
|
|
1357
|
+
border: '1px solid var(--dsw-alias-border-secondary, #3a3a42)',
|
|
1358
|
+
background: 'transparent', color: 'inherit',
|
|
1359
|
+
cursor: hiddenIds.size === 0 ? 'default' : 'pointer',
|
|
1360
|
+
opacity: hiddenIds.size === 0 ? 0.45 : 1,
|
|
1361
|
+
},
|
|
1362
|
+
onClick: () => { unhideAll(); setSettings({ ...settings, msg: '已恢复全部隐藏的会话' }); },
|
|
1363
|
+
}, '恢复全部隐藏的会话'),
|
|
1364
|
+
h('div', { className: 'pr-hint', style: { fontSize: '10px' } },
|
|
1365
|
+
'(这些是归档时报 no such session 的幽灵会话,磁盘上已不存在;恢复后会重新出现在侧栏)'),
|
|
1299
1366
|
h('div', { className: 'pr-hint', style: { marginTop: '10px' } }, '显示'),
|
|
1300
1367
|
h('label', { style: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '11px', cursor: 'pointer', padding: '3px 0' } },
|
|
1301
1368
|
h('input', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-project-room",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Project rooms for DeepSeek Harness: group sessions into projects, see per-project runtime, and open a room with a shared board, task list, todo queue and message delivery to member sessions.",
|
|
6
6
|
"main": "index.js",
|