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.
Files changed (2) hide show
  1. package/client.js +101 -34
  2. 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
- // 归档必然报 no such session(实测 25/25 全失败)。这类幽灵无法删除,只能不显示。
824
- const [aliveIds, setAliveIds] = React.useState(null);
825
- React.useEffect(() => {
826
- let cancelled = false;
827
- const load = async () => {
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
- const res = await fetch(`${API}/alive`);
830
- const data = await res.json();
831
- if (!cancelled && data && data.ok && Array.isArray(data.ids)) {
832
- setAliveIds(new Set(data.ids.map((v) => String(v).toLowerCase())));
833
- }
834
- } catch { /* 拿不到白名单就不过滤 */ }
835
- };
836
- load();
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
- // 2026-11-10 用户报「自测乙 / 权限自测 一直删不掉」:这类条目只活在前端快照里,
857
- // 磁盘会话目录早已消失 → 归档必然报 no such session,永远清不掉。
858
- // 「未分组」区早已按 alive 白名单过滤(见 looseMembers),项目分组这里漏了,补上。
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, aliveIds]);
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
- const looseMembers = aliveIds
1004
- ? looseAll.filter((m) => aliveIds.has(String(m.id).replace(/^session-/, '').toLowerCase()))
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
- const runMember = (group.running.length ? group.running : group.members)
1094
- .slice()
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 && runMember && startedAt ? formatDuration(tick - startedAt) : '';
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
- + (done ? ' · 完成待查看' : (elapsed ? ` · 运行中 ${elapsed}` : ' · 运行中'))
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
- : h('span', { className: 'pr-running-dot', title: '运行中' }),
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
- const failNote = Array.isArray(data.failed) && data.failed.length
1221
- ? `,失败 ${data.failed.length}(${String((data.failed[0] && data.failed[0].error) || '').slice(0, 60)})`
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
- setMenuMsg(data && data.ok && data.archived ? '已归档' : `失败:${(data && data.failed && data.failed[0] && data.failed[0].error) || (data && data.error) || '未知'}`.slice(0, 80));
1264
- } catch (error) { setMenuMsg(`失败:${String((error && error.message) || error)}`.slice(0, 80)); }
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.0",
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",