dsh-whaleling 0.0.0-stage → 0.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.
@@ -0,0 +1,4 @@
1
+ # dsh-whaleling:把 host 里的活会话状态写给悬浮窗。
2
+ - insert:
3
+ - id: whaleling
4
+ name: dsh-whaleling
package/icon.svg ADDED
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <rect x="4" y="4" width="56" height="56" rx="14" fill="#1b2030"/>
3
+ <path d="M14 40c6-12 14-12 18 0" fill="none" stroke="#4d6bfe" stroke-width="4" stroke-linecap="round"/>
4
+ <path d="M32 40c6-12 14-12 18 0" fill="none" stroke="#8fa6ff" stroke-width="4" stroke-linecap="round"/>
5
+ <circle cx="32" cy="22" r="3.2" fill="#fff"/>
6
+ </svg>
package/index.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * dshfloat-bridge —— 把「哪个会话正在跑」从 host 进程里递出来。
3
+ *
4
+ * 为什么需要它:悬浮窗是独立进程,拿不到 ctx.*;而读磁盘只能靠日志 mtime 猜,
5
+ * 猜不准(一轮里可能几十秒没有写入)。这个插件在 host 里用 ctx.sessions.list()
6
+ * 拿到真实的活会话列表,增量读事件判断 turn 是否结束,再把结果写成一个小 JSON。
7
+ *
8
+ * 输出:~/.dsh/dshfloat-bridge.json
9
+ * { v, at, running, sessions, ids }
10
+ * 写不进去就算了 —— 悬浮窗读不到会自动退回读盘。
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, renameSync } from 'node:fs'
14
+ import { homedir } from 'node:os'
15
+ import { join } from 'node:path'
16
+
17
+ export const name = 'dshfloat-bridge'
18
+
19
+ export const inject = ['sessions']
20
+
21
+ const FILE = join(homedir(), '.dsh', 'dshfloat-bridge.json')
22
+ const REQ_FILE = join(homedir(), '.dsh', 'dshfloat-request.json')
23
+ const REPLY_FILE = join(homedir(), '.dsh', 'dshfloat-reply.json')
24
+ const KEEP_ENDS = 200 // 最近多少条 turn/end 递出去
25
+ const KEEP_MS = 24 * 3600e3 // 更早的丢掉
26
+ const TEXT_LIMIT = 20000 // 按需返回时用
27
+ const MAX_SESSIONS = 120
28
+
29
+ export function apply(ctx) {
30
+ /** sessionId -> { seq, open, title, lastAssistant, lastUser, at } */
31
+ const state = new Map()
32
+ /** 最近的 turn/end:widget 靠它按事件时间判断「当时我在不在 DSH」 */
33
+ const ends = []
34
+ let servedNonce = ''
35
+
36
+ const tick = () => {
37
+ let running = 0
38
+ let total = 0
39
+ const ids = []
40
+
41
+ for (const session of ctx.sessions.list()) {
42
+ total += 1
43
+ const id = String(session.id)
44
+ const prev = state.get(id) ?? {
45
+ seq: 0, open: false, title: '', lastAssistant: '', lastUser: '', at: 0,
46
+ cwd: String(session.header?.cwd ?? ''),
47
+ }
48
+
49
+ if (session.seq > prev.seq) {
50
+ // 只读上次之后的新事件,不整份重放
51
+ for (const event of session.snapshotEvents(prev.seq, session.seq)) {
52
+ if (event.type === 'turn/start') {
53
+ prev.open = true
54
+ prev.at = Number(event.time)
55
+ } else if (event.type === 'assistant/message') {
56
+ const text = (event.data?.message?.content ?? [])
57
+ .filter((block) => block?.type === 'text')
58
+ .map((block) => block.text)
59
+ .join('')
60
+ .trim()
61
+ if (text) {
62
+ prev.lastAssistant = text.slice(0, TEXT_LIMIT)
63
+ prev.at = Number(event.time)
64
+ }
65
+ } else if (event.type === 'user/message') {
66
+ const text = (event.data?.content ?? [])
67
+ .filter((block) => block?.type === 'text')
68
+ .map((block) => block.text)
69
+ .join('')
70
+ .trim()
71
+ if (text) prev.lastUser = text.slice(0, TEXT_LIMIT)
72
+ } else if (event.type === 'session/title') {
73
+ const title = event.data?.title
74
+ if (typeof title === 'string' && title) prev.title = title
75
+ } else if (event.type === 'turn/end') {
76
+ prev.open = false
77
+ prev.at = Number(event.time)
78
+ ends.push({
79
+ id,
80
+ seq: Number(event.seq),
81
+ time: Number(event.time),
82
+ reason: event.data?.reason?.kind ?? 'unknown',
83
+ })
84
+ }
85
+ }
86
+ prev.seq = session.seq
87
+ state.set(id, prev)
88
+ }
89
+
90
+ if (prev.open) {
91
+ running += 1
92
+ ids.push(id)
93
+ }
94
+ }
95
+
96
+ // 只留最近的一批;按时间戳裁剪,避免无限增长
97
+ const cutoff = Date.now() - KEEP_MS
98
+ while (ends.length > 0 && (ends[0].time < cutoff || ends.length > KEEP_ENDS)) ends.shift()
99
+
100
+ const list = []
101
+ for (const [id, value] of state) {
102
+ if (!value.at && !value.lastAssistant && !value.title) continue
103
+ list.push({
104
+ id,
105
+ cwd: value.cwd || '',
106
+ title: value.title || '',
107
+ running: value.open,
108
+ at: value.at,
109
+ })
110
+ }
111
+ list.sort((a, b) => b.at - a.at)
112
+
113
+ // 只在你点开某个会话时,才把它的正文写出来
114
+ try {
115
+ const req = JSON.parse(readFileSync(REQ_FILE, 'utf8'))
116
+ if (req && req.nonce && req.nonce !== servedNonce) {
117
+ servedNonce = req.nonce
118
+ const value = state.get(String(req.id)) || {}
119
+ writeFileSync(REPLY_FILE + '.tmp', JSON.stringify({
120
+ nonce: req.nonce,
121
+ id: req.id,
122
+ title: value.title || '',
123
+ assistant: value.lastAssistant || '',
124
+ user: value.lastUser || '',
125
+ }))
126
+ renameSync(REPLY_FILE + '.tmp', REPLY_FILE)
127
+ }
128
+ } catch {
129
+ // 没有请求就跳过
130
+ }
131
+
132
+ const payload = JSON.stringify({
133
+ v: 3,
134
+ at: Date.now(),
135
+ running,
136
+ sessions: total,
137
+ ids,
138
+ ends,
139
+ list: list.slice(0, MAX_SESSIONS),
140
+ })
141
+ try {
142
+ writeFileSync(`${FILE}.tmp`, payload)
143
+ renameSync(`${FILE}.tmp`, FILE)
144
+ } catch {
145
+ // 静默失败:widget 那边会退回读盘
146
+ }
147
+ }
148
+
149
+ ctx.effect(() => {
150
+ const timer = setInterval(tick, 1000)
151
+ tick()
152
+ return () => clearInterval(timer)
153
+ })
154
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "DshFloat Bridge",
4
+ "description": "Writes live session state to ~/.dsh/dshfloat-bridge.json for the floating widget."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "DshFloat Bridge",
4
+ "description": "把当前活跃会话状态写到 ~/.dsh/dshfloat-bridge.json,供悬浮窗读取。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "dsh-whaleling",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Bridge plugin that exposes live DeepSeek Harness session state to the whaleling floating widget.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./locale/*.json": "./locale/*.json",
10
+ "./icon": "./icon.svg"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "cordis.patch.yml",
15
+ "locale/*.json",
16
+ "icon.svg"
17
+ ],
18
+ "keywords": [
19
+ "dsh",
20
+ "deepseek-harness",
21
+ "plugin",
22
+ "dsh-bundle"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/rUrU516/dsh-whaleling.git",
27
+ "directory": "bundles/dsh-whaleling"
28
+ },
29
+ "engines": {
30
+ "dsh": ">=0.2.0 <0.3.0"
31
+ },
32
+ "dsh": {
33
+ "bundle": {
34
+ "patch": "./cordis.patch.yml"
35
+ }
36
+ },
37
+ "license": "MIT"
38
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.