dsh-sessions-manager 3.5.0 → 3.5.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/README.en.md +2 -4
- package/README.md +2 -4
- package/lib/client.js +18 -4
- package/lib/client.js.map +2 -2
- package/lib/index.js +92 -29
- package/lib/index.js.map +3 -3
- package/package.json +1 -1
- package/src/client/index.jsx +16 -7
- package/src/client/logic.js +8 -0
- package/src/index.js +26 -5
- package/src/zstd-frame.js +68 -34
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sessions-manager",
|
|
3
3
|
"description": "DSH 设置面板会话管理器:归档 / 恢复 / 彻底删除 / 移动到其他工作区,带工作区标签与会话日期;统一「会话管理」面板。Session manager for the DeepSeek Harness settings panel — archive / restore / permanently delete / move sessions across workspaces, with workspace tags & session dates in one unified panel.",
|
|
4
|
-
"version": "3.5.
|
|
4
|
+
"version": "3.5.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/client/index.jsx
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
14
|
-
import { canDropOnWorkspace, dotStateFor, sessionForNodes, starredOf, workspaceForNodes } from './logic.js'
|
|
14
|
+
import { authoritativeTitleForFirstPaint, canDropOnWorkspace, dotStateFor, sessionForNodes, starredOf, workspaceForNodes } from './logic.js'
|
|
15
15
|
|
|
16
16
|
export const inject = ['slots']
|
|
17
17
|
|
|
@@ -1129,7 +1129,7 @@ const SIDEBAR_AUG_CSS = `
|
|
|
1129
1129
|
.dsm-sub-cur{font-size:11px;color:var(--dsw-alias-state-business-primary);flex:none;margin-left:8px}
|
|
1130
1130
|
.dsm-dot{position:absolute;left:6px;top:50%;transform:translateY(-50%);width:8px;height:8px;border-radius:50%;box-sizing:border-box;cursor:pointer;pointer-events:auto;z-index:1}
|
|
1131
1131
|
.dsm-dot-unread-manual{background:var(--dsw-alias-state-business-primary)}
|
|
1132
|
-
.dsm-dot-waiting{background:var(--dsw-alias-state-
|
|
1132
|
+
.dsm-dot-waiting{background:var(--dsw-alias-state-warn-primary,#F59E0B)}
|
|
1133
1133
|
.dsm-dot-unread{background:var(--dsw-alias-state-success-primary)}
|
|
1134
1134
|
.dsm-drag-source{opacity:.48}
|
|
1135
1135
|
.dsm-drop-target{position:relative;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)!important;outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px;border-radius:8px}
|
|
@@ -1465,7 +1465,7 @@ function installSidebarSessionMenuAug() {
|
|
|
1465
1465
|
// Left-side per-session status dot (DOM shim). The color is driven by DSH's
|
|
1466
1466
|
// REAL session status, read from the row's own StateDot ([data-state]):
|
|
1467
1467
|
// 手动标记未读(蓝) = 用户在 ⋯ 菜单或点击圆点手动标记,localStorage 持久化(最高优先级)
|
|
1468
|
-
// 工作中(黄) = DSH state 'running'
|
|
1468
|
+
// 工作中(黄) = DSH state 'ongoing'(旧版兼容 'running')
|
|
1469
1469
|
// 需用户反馈(琥珀) = DSH state 'warning'(有追问需用户反馈)
|
|
1470
1470
|
// 完成后未读(绿) = DSH state 'done' 且用户尚未读过(read 集合)
|
|
1471
1471
|
// 完成已读(不显示) = DSH state 'done' 且已读过(read 集合持久化)
|
|
@@ -1503,7 +1503,7 @@ function installSidebarStatusDots() {
|
|
|
1503
1503
|
}
|
|
1504
1504
|
// Dot colors per the user's scheme. We read DSH's REAL session status from the
|
|
1505
1505
|
// row's own StateDot (its [data-state] attr) and recolor it:
|
|
1506
|
-
// running
|
|
1506
|
+
// ongoing/running → 黄 工作中
|
|
1507
1507
|
// warning → 琥珀 有追问需用户反馈
|
|
1508
1508
|
// done → 绿 完成后未读(读过则不再显示)
|
|
1509
1509
|
// error → 红 出错/需关注(与 DSH 自带 StateDot 一致)
|
|
@@ -1511,11 +1511,12 @@ function installSidebarStatusDots() {
|
|
|
1511
1511
|
const COLOR = {
|
|
1512
1512
|
manual: 'var(--dsw-alias-state-business-primary)', // 蓝 手动标记未读
|
|
1513
1513
|
running: '#EAB308', // 黄 工作中 (DSH running)
|
|
1514
|
-
feedback: 'var(--dsw-alias-state-
|
|
1514
|
+
feedback: 'var(--dsw-alias-state-warn-primary, #F59E0B)', // 琥珀 需用户反馈 (DSH warning)
|
|
1515
1515
|
done: 'var(--dsw-alias-state-success-primary)', // 绿 完成后未读 (DSH done)
|
|
1516
1516
|
error: 'var(--dsw-alias-state-error-primary)', // 红 出错/需关注 (DSH error)
|
|
1517
1517
|
}
|
|
1518
1518
|
const manualUnread = dsmLoadManual()
|
|
1519
|
+
const titleInitializedRows = new WeakSet()
|
|
1519
1520
|
const paint = () => {
|
|
1520
1521
|
// Recompute the active session on every paint so the active row is never
|
|
1521
1522
|
// shown from a stale `curActive` (the click that changes aria-selected
|
|
@@ -1549,12 +1550,20 @@ function installSidebarStatusDots() {
|
|
|
1549
1550
|
// Session is opened. Paint the latest log-folded title from the host
|
|
1550
1551
|
// authority without materializing the Session or changing its log.
|
|
1551
1552
|
const authoritativeTitle = dsmAuthoritativeTitles.get(id)
|
|
1552
|
-
if (
|
|
1553
|
+
if (!titleInitializedRows.has(row)) {
|
|
1553
1554
|
const node = rowNode(row) || {}
|
|
1554
1555
|
const expected = new Set([node.title, node.displayTitle, node.name].filter((value) => typeof value === 'string'))
|
|
1555
1556
|
const spans = [...row.children].filter((el) => el.tagName === 'SPAN' && !el.querySelector('[data-state]') && (el.textContent || '').trim())
|
|
1556
1557
|
const titleEl = spans.find((el) => expected.has((el.textContent || '').trim())) || spans[0]
|
|
1557
|
-
if (titleEl
|
|
1558
|
+
if (titleEl) {
|
|
1559
|
+
const correction = authoritativeTitleForFirstPaint({
|
|
1560
|
+
firstPaint: true,
|
|
1561
|
+
rendered: titleEl.textContent || '',
|
|
1562
|
+
authoritative: authoritativeTitle || '',
|
|
1563
|
+
})
|
|
1564
|
+
if (correction) titleEl.textContent = correction
|
|
1565
|
+
titleInitializedRows.add(row)
|
|
1566
|
+
}
|
|
1558
1567
|
}
|
|
1559
1568
|
// Read DSH's REAL session status from the row's own StateDot and recolor
|
|
1560
1569
|
// it with the user's scheme (we also hide DSH's dot so only ours shows).
|
package/src/client/logic.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export function dotStateFor({ manualUnread = false, dataState = '', isActive = false } = {}) {
|
|
11
11
|
if (manualUnread) return 'manual'
|
|
12
12
|
switch (dataState) {
|
|
13
|
+
case 'ongoing':
|
|
13
14
|
case 'running': return 'running'
|
|
14
15
|
case 'warning': return 'feedback'
|
|
15
16
|
case 'error': return 'error'
|
|
@@ -18,6 +19,13 @@ export function dotStateFor({ manualUnread = false, dataState = '', isActive = f
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
// 权威标题只修正新出现 DOM 行的冷态标题。行一旦完成首次 paint,后续文本
|
|
23
|
+
// 变化属于 DSH Core 的实时更新(例如重命名),插件不得再用轮询快照覆盖。
|
|
24
|
+
export function authoritativeTitleForFirstPaint({ firstPaint = false, rendered = '', authoritative = '' } = {}) {
|
|
25
|
+
if (!firstPaint || !authoritative || rendered === authoritative) return null
|
|
26
|
+
return authoritative
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
// 拖拽迁移前置校验:同工作区拦截(workspacePath 相等即拒绝)。
|
|
22
30
|
// 无 workspacePath 的会话(如侧栏 live 行尚未同步)放行,由 host 最终裁决。
|
|
23
31
|
export function canDropOnWorkspace(item, target) {
|
package/src/index.js
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// (workspaceRegistry + storageDomain), folds titles/dates/workspace tags from
|
|
8
8
|
// session persistence, physically removes a session's log file on delete, and
|
|
9
9
|
// relocates a conversation (session) between workspaces on move.
|
|
10
|
-
import { mkdir, realpath, rename, stat, unlink, writeFile } from 'node:fs/promises'
|
|
10
|
+
import { mkdir, readFile, realpath, rename, stat, unlink, writeFile } from 'node:fs/promises'
|
|
11
11
|
import { basename, dirname, isAbsolute, join } from 'node:path'
|
|
12
12
|
import { readFileSync } from 'node:fs'
|
|
13
13
|
import { homedir } from 'node:os'
|
|
14
|
-
import {
|
|
14
|
+
import { rewriteFrame0CwdInMemory, scanZstdFrames } from './zstd-frame.js'
|
|
15
15
|
import { renderSessionMarkdown } from './markdown.js'
|
|
16
16
|
import { createStarIndex } from './star-index.js'
|
|
17
17
|
import { aggregateStorage } from './storage-stats.js'
|
|
@@ -637,15 +637,36 @@ export function apply(ctx) {
|
|
|
637
637
|
const newPath = locatePath(newHeaderObj)
|
|
638
638
|
if (!oldPath || !newPath || oldPath === newPath) return false
|
|
639
639
|
const backupPath = `${oldPath}.move-backup-${Date.now()}`
|
|
640
|
+
const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`
|
|
641
|
+
let destinationInstalled = false
|
|
640
642
|
try {
|
|
641
643
|
// Ensure the destination project directory exists (rename does not
|
|
642
644
|
// create it). Without this, the rename silently no-ops on ENOENT and
|
|
643
645
|
// the log stays put while workspace.json is wrongly updated.
|
|
644
646
|
await mkdir(dirname(newPath), { recursive: true })
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
647
|
+
try {
|
|
648
|
+
await stat(newPath)
|
|
649
|
+
throw new Error('移动失败:目标位置已存在同名会话日志')
|
|
650
|
+
} catch (e) {
|
|
651
|
+
if (e && e.code !== 'ENOENT') throw e
|
|
652
|
+
}
|
|
653
|
+
await rename(oldPath, backupPath) // keep the original byte-identical until verification succeeds
|
|
654
|
+
const original = await readFile(backupPath)
|
|
655
|
+
const originalFrames = scanZstdFrames(original).frames
|
|
656
|
+
if (originalFrames.length === 0) throw new Error('移动前校验失败:会话日志没有完整 zstd 帧')
|
|
657
|
+
const rewritten = rewriteFrame0CwdInMemory(original, canonical)
|
|
658
|
+
const rewrittenFrames = scanZstdFrames(rewritten).frames
|
|
659
|
+
if (rewrittenFrames.length !== originalFrames.length) throw new Error('移动后校验失败:会话日志帧数发生变化')
|
|
660
|
+
const originalTail = original.subarray(originalFrames[0].end)
|
|
661
|
+
const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end)
|
|
662
|
+
if (!originalTail.equals(rewrittenTail)) throw new Error('移动后校验失败:会话事件内容发生变化')
|
|
663
|
+
await writeFile(stagedPath, rewritten, { mode: 0o600 })
|
|
664
|
+
await rename(stagedPath, newPath)
|
|
665
|
+
destinationInstalled = true
|
|
666
|
+
await unlink(backupPath)
|
|
648
667
|
} catch (e) {
|
|
668
|
+
try { await unlink(stagedPath) } catch (_) {}
|
|
669
|
+
if (destinationInstalled) { try { await unlink(newPath) } catch (_) {} }
|
|
649
670
|
try { await rename(backupPath, oldPath) } catch (_) {}
|
|
650
671
|
if (e && e.code !== 'ENOENT') throw e
|
|
651
672
|
return false
|
package/src/zstd-frame.js
CHANGED
|
@@ -20,33 +20,71 @@ export const ZSTD_MAGIC = 0xFD2FB528
|
|
|
20
20
|
const CHECKSUM_OPTS = { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } }
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* sequence can occur inside compressed data. Every candidate is therefore
|
|
27
|
-
* validated by attempting decompression; only offsets that decode are kept.
|
|
23
|
+
* Parse complete concatenated Zstandard frames without decompressing them.
|
|
24
|
+
* Invalid complete structure rejects; EOF inside the final frame is reported
|
|
25
|
+
* as torn rather than guessed from magic bytes occurring in compressed data.
|
|
28
26
|
*
|
|
29
27
|
* @param {Buffer} buf
|
|
30
|
-
* @
|
|
28
|
+
* @param {number} maxFrames
|
|
29
|
+
* @returns {{frames: Array<{start:number,end:number}>, tornStart?: number}}
|
|
31
30
|
*/
|
|
32
|
-
export function
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
export function scanZstdFrames(buf, maxFrames = Number.POSITIVE_INFINITY) {
|
|
32
|
+
const frames = []
|
|
33
|
+
let offset = 0
|
|
34
|
+
while (offset < buf.length) {
|
|
35
|
+
const start = offset
|
|
36
|
+
if (buf.length - offset < 4) return { frames, tornStart: start }
|
|
37
|
+
if (buf.readUInt32LE(offset) !== ZSTD_MAGIC) {
|
|
38
|
+
throw new Error(`会话日志格式异常(字节 ${offset} 的 zstd magic 无效)`)
|
|
39
|
+
}
|
|
40
|
+
offset += 4
|
|
41
|
+
if (offset === buf.length) return { frames, tornStart: start }
|
|
42
|
+
|
|
43
|
+
const descriptor = buf.readUInt8(offset++)
|
|
44
|
+
if ((descriptor & 0x18) !== 0) throw new Error(`会话日志格式异常(字节 ${offset - 1} 使用保留帧头位)`)
|
|
45
|
+
const contentSizeFlag = descriptor >>> 6
|
|
46
|
+
const singleSegment = (descriptor & 0x20) !== 0
|
|
47
|
+
const checksum = (descriptor & 0x04) !== 0
|
|
48
|
+
const dictionaryFlag = descriptor & 0x03
|
|
49
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag
|
|
50
|
+
const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << contentSizeFlag
|
|
51
|
+
const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes
|
|
52
|
+
if (buf.length - offset < remainingHeaderBytes) return { frames, tornStart: start }
|
|
53
|
+
offset += remainingHeaderBytes
|
|
54
|
+
|
|
55
|
+
for (;;) {
|
|
56
|
+
if (buf.length - offset < 3) return { frames, tornStart: start }
|
|
57
|
+
const blockHeader = buf.readUIntLE(offset, 3)
|
|
58
|
+
offset += 3
|
|
59
|
+
const lastBlock = (blockHeader & 1) !== 0
|
|
60
|
+
const blockType = (blockHeader >>> 1) & 0x03
|
|
61
|
+
const blockSize = blockHeader >>> 3
|
|
62
|
+
if (blockType === 0x03) throw new Error(`会话日志格式异常(字节 ${offset - 3} 使用保留块类型)`)
|
|
63
|
+
const payloadBytes = blockType === 0x01 ? 1 : blockSize
|
|
64
|
+
if (buf.length - offset < payloadBytes) return { frames, tornStart: start }
|
|
65
|
+
offset += payloadBytes
|
|
66
|
+
if (lastBlock) break
|
|
67
|
+
}
|
|
68
|
+
if (checksum) {
|
|
69
|
+
if (buf.length - offset < 4) return { frames, tornStart: start }
|
|
70
|
+
offset += 4
|
|
47
71
|
}
|
|
72
|
+
frames.push({ start, end: offset })
|
|
73
|
+
if (frames.length === maxFrames) return { frames }
|
|
48
74
|
}
|
|
49
|
-
return
|
|
75
|
+
return { frames }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Backward-compatible frame-start view used by diagnostics and tests. */
|
|
79
|
+
export function findZstdFrameStarts(buf) {
|
|
80
|
+
return scanZstdFrames(buf).frames.map((frame) => frame.start)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function firstFrame(buf) {
|
|
84
|
+
const scan = scanZstdFrames(buf, 1)
|
|
85
|
+
const frame = scan.frames[0]
|
|
86
|
+
if (!frame) throw new Error('会话日志格式异常(无完整 zstd 帧)')
|
|
87
|
+
return frame
|
|
50
88
|
}
|
|
51
89
|
|
|
52
90
|
/**
|
|
@@ -64,10 +102,9 @@ export function findZstdFrameStarts(buf) {
|
|
|
64
102
|
*/
|
|
65
103
|
export function rewriteFrame0Cwd(filePath, newCwd) {
|
|
66
104
|
const buf = readFileSync(filePath)
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const frame0 = buf.subarray(starts[0], end0)
|
|
105
|
+
const frame = firstFrame(buf)
|
|
106
|
+
const end0 = frame.end
|
|
107
|
+
const frame0 = buf.subarray(frame.start, end0)
|
|
71
108
|
const text = zlib.zstdDecompressSync(frame0).toString('utf8')
|
|
72
109
|
const nl = text.indexOf('\n')
|
|
73
110
|
const line = nl >= 0 ? text.slice(0, nl) : text
|
|
@@ -91,10 +128,9 @@ export function rewriteFrame0Cwd(filePath, newCwd) {
|
|
|
91
128
|
* @returns {Buffer} rewritten log
|
|
92
129
|
*/
|
|
93
130
|
export function rewriteFrame0CwdInMemory(buf, newCwd) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const frame0 = buf.subarray(starts[0], end0)
|
|
131
|
+
const frame = firstFrame(buf)
|
|
132
|
+
const end0 = frame.end
|
|
133
|
+
const frame0 = buf.subarray(frame.start, end0)
|
|
98
134
|
const text = zlib.zstdDecompressSync(frame0).toString('utf8')
|
|
99
135
|
const nl = text.indexOf('\n')
|
|
100
136
|
const line = nl >= 0 ? text.slice(0, nl) : text
|
|
@@ -128,10 +164,8 @@ export function buildSessionLog(header, events = []) {
|
|
|
128
164
|
* @returns {{obj: object, lineCount: number}}
|
|
129
165
|
*/
|
|
130
166
|
export function readFrame0(buf) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
const end0 = starts.length > 1 ? starts[1] : buf.length
|
|
134
|
-
const text = zlib.zstdDecompressSync(buf.subarray(starts[0], end0)).toString('utf8')
|
|
167
|
+
const frame = firstFrame(buf)
|
|
168
|
+
const text = zlib.zstdDecompressSync(buf.subarray(frame.start, frame.end)).toString('utf8')
|
|
135
169
|
const lines = text.split('\n').filter((l) => l.length > 0)
|
|
136
170
|
return { obj: JSON.parse(lines[0]), lineCount: lines.length }
|
|
137
171
|
}
|