dsh-island-overlay 0.2.2 → 0.2.3
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 +3 -2
- package/lib/index.js +7 -4
- package/overlay/activate.ps1 +4 -1
- package/overlay/index.html +19 -7
- package/overlay/main.js +32 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,15 +83,16 @@ dsh plugin --profile desktop add dsh-island-overlay
|
|
|
83
83
|
| 事件 | 显示 |
|
|
84
84
|
|---|---|
|
|
85
85
|
| `turn/start` | 思考(小鲸鱼 + 计时,2.5s 后缩成小图标) |
|
|
86
|
-
| `approval/asked` |
|
|
86
|
+
| `approval/asked` | 等待审批(琥珀脉冲,常驻展开,计时接着走) |
|
|
87
87
|
| `tool/call` = `ask_user_question` | 等待回答(同上) |
|
|
88
88
|
| `turn/end` = `completed` | 完成(绿勾,3s 后消失) |
|
|
89
89
|
| `turn/end` = `error` / `blocked` / `max-tokens` | 失败(红色抖动,6s 后消失) |
|
|
90
90
|
| `turn/end` = `aborted` / `interrupted` / `forked` | 不显示,直接消失 |
|
|
91
91
|
|
|
92
92
|
多个会话同时有事时只显示优先级最高的一个(审批/回答 > 失败 > 完成 > 思考),其余用 `+N` 角标;最多并列显示 3 个会话,每段各自计时。
|
|
93
|
+
计时读的是**回合开始时间**,所以「思考 20 分钟 → 弹审批 → 继续思考」是一条连续的时间轴,不会归零。
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
交互:拖动可移动位置(自动切到「自定义位置」,记在 `~/.dsh/dsh-island-pos.json`);点一下把 DSH 拉到前台;
|
|
95
96
|
鼠标划过展开全文;检测到前台是全屏程序(视频/游戏)时自动隐藏。
|
|
96
97
|
|
|
97
98
|
## 卸载
|
package/lib/index.js
CHANGED
|
@@ -167,24 +167,27 @@ export default {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
const name = titleOf(session)
|
|
170
|
+
// since = 这一回合的开始时间,只有 turn/start 能改它。
|
|
171
|
+
// 以前 approval/asked、approval/decided、tool/call 都把它重置成 now,
|
|
172
|
+
// 于是「思考 20 分钟 → 弹审批 → 继续思考」的计时会从 0 重新数。
|
|
170
173
|
switch (type) {
|
|
171
174
|
case 'turn/start':
|
|
172
175
|
touch(sid, { state: 'thinking', detail: '', since: Date.now(), title: name })
|
|
173
176
|
break
|
|
174
177
|
case 'approval/asked':
|
|
175
|
-
touch(sid, { state: 'approval', detail: String(data.toolName || '需要授权'),
|
|
178
|
+
touch(sid, { state: 'approval', detail: String(data.toolName || '需要授权'), title: name })
|
|
176
179
|
break
|
|
177
180
|
case 'approval/decided':
|
|
178
|
-
if (items.has(sid)) touch(sid, { state: 'thinking', detail: '',
|
|
181
|
+
if (items.has(sid)) touch(sid, { state: 'thinking', detail: '', title: name })
|
|
179
182
|
break
|
|
180
183
|
case 'tool/call':
|
|
181
184
|
if (String(data.name || '') === 'ask_user_question') {
|
|
182
|
-
touch(sid, { state: 'ask', detail: '等待你的回答',
|
|
185
|
+
touch(sid, { state: 'ask', detail: '等待你的回答', title: name })
|
|
183
186
|
}
|
|
184
187
|
break
|
|
185
188
|
case 'tool/result': {
|
|
186
189
|
const cur = items.get(sid)
|
|
187
|
-
if (cur && cur.state === 'ask') touch(sid, { state: 'thinking', detail: '',
|
|
190
|
+
if (cur && cur.state === 'ask') touch(sid, { state: 'thinking', detail: '', title: name })
|
|
188
191
|
break
|
|
189
192
|
}
|
|
190
193
|
case 'turn/end': {
|
package/overlay/activate.ps1
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Bring the DSH main window to the foreground (click-through pill -> click).
|
|
2
|
+
# Fallback path only: the overlay normally sends "activate" over watch.ps1's stdin.
|
|
2
3
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
3
4
|
|
|
4
5
|
Add-Type -TypeDefinition @"
|
|
@@ -7,6 +8,7 @@ using System.Runtime.InteropServices;
|
|
|
7
8
|
public class IslandFg {
|
|
8
9
|
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
|
9
10
|
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
|
11
|
+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
|
|
10
12
|
[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, IntPtr extra);
|
|
11
13
|
}
|
|
12
14
|
"@
|
|
@@ -20,5 +22,6 @@ if (-not $p) { exit }
|
|
|
20
22
|
# 先按一下 Alt 骗过前台锁定,再抢焦点
|
|
21
23
|
[IslandFg]::keybd_event(0x12, 0, 0, [IntPtr]::Zero)
|
|
22
24
|
[IslandFg]::keybd_event(0x12, 0, 2, [IntPtr]::Zero)
|
|
23
|
-
|
|
25
|
+
# 只在真最小化时还原。SW_RESTORE 会把最大化窗口缩回普通窗口,就是用户报的「点了变成窗口化」
|
|
26
|
+
if ([IslandFg]::IsIconic($p.MainWindowHandle)) { [IslandFg]::ShowWindow($p.MainWindowHandle, 9) | Out-Null }
|
|
24
27
|
[IslandFg]::SetForegroundWindow($p.MainWindowHandle) | Out-Null
|
package/overlay/index.html
CHANGED
|
@@ -150,7 +150,12 @@ function esc(s) {
|
|
|
150
150
|
|
|
151
151
|
function fmtTime(sec) {
|
|
152
152
|
sec = Math.max(0, Math.floor(sec))
|
|
153
|
-
|
|
153
|
+
if (sec < 60) return sec + 's'
|
|
154
|
+
const m = Math.floor(sec / 60)
|
|
155
|
+
const s = sec % 60
|
|
156
|
+
if (m < 60) return s ? m + 'm' + s + 's' : m + 'm'
|
|
157
|
+
const h = Math.floor(m / 60)
|
|
158
|
+
return m % 60 ? h + 'h' + (m % 60) + 'm' : h + 'h'
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
function icon(state) {
|
|
@@ -202,8 +207,11 @@ function ordered() {
|
|
|
202
207
|
.sort((a, b) => (RANK[b.state] || 0) - (RANK[a.state] || 0) || (a.since || 0) - (b.since || 0))
|
|
203
208
|
}
|
|
204
209
|
|
|
210
|
+
const TIMED = { thinking: 1, approval: 1, ask: 1 }
|
|
211
|
+
|
|
205
212
|
function segHtml(it, primary) {
|
|
206
|
-
|
|
213
|
+
// 计时都读 since(= 回合开始),所以「思考 → 审批 → 继续思考」是一条连续的时间轴
|
|
214
|
+
const meta = TIMED[it.state]
|
|
207
215
|
? '<span class="meta" data-since="' + (Number(it.since) || Date.now()) + '">' + fmtTime((Date.now() - (Number(it.since) || Date.now())) / 1000) + '</span>'
|
|
208
216
|
: it.state === 'done' ? '<span class="meta">刚刚</span>' : ''
|
|
209
217
|
// 只有主段展开标题,其余段只留图标 + 状态词,避免胶囊被撑爆
|
|
@@ -228,6 +236,9 @@ function render() {
|
|
|
228
236
|
curState = ''
|
|
229
237
|
clearTimeout(shrinkTimer)
|
|
230
238
|
if (tick) { clearInterval(tick); tick = null }
|
|
239
|
+
// 悬停中被清空时必须把鼠标穿透还回去:窗口本身还在,ignoreMouseEvents=false
|
|
240
|
+
// 会让它在「看不见」的状态下继续吃掉顶部中间那块区域的点击。
|
|
241
|
+
if (hovered) { hovered = false; window.islandApi.hover(false) }
|
|
231
242
|
return
|
|
232
243
|
}
|
|
233
244
|
|
|
@@ -235,8 +246,9 @@ function render() {
|
|
|
235
246
|
const e = cfg.effects || DEFAULTS.effects
|
|
236
247
|
const shown = list.slice(0, MAXSEG)
|
|
237
248
|
const rest = list.length - shown.length
|
|
238
|
-
// cfg 也进 key:设置页改了缩放/迷你尺寸/动效开关时,items
|
|
239
|
-
|
|
249
|
+
// cfg 也进 key:设置页改了缩放/迷你尺寸/动效开关时,items 没变也得重画。
|
|
250
|
+
// title/detail 同样要进:会话改名、失败原因变化时 since 不变,漏了就不会重绘。
|
|
251
|
+
const key = list.map(it => it.state + it.id + it.since + '|' + (it.title || '') + '|' + (it.detail || '')).join(',') + '|' + rest + '|' + JSON.stringify(cfg)
|
|
240
252
|
if (key !== curKey) {
|
|
241
253
|
curKey = key
|
|
242
254
|
curState = best.state
|
|
@@ -255,9 +267,9 @@ function render() {
|
|
|
255
267
|
scheduleMini(hovered ? 0 : holdFor(best.state))
|
|
256
268
|
}
|
|
257
269
|
|
|
258
|
-
//
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
270
|
+
// 只要有一段带计时(思考/审批/等待回答)就跑秒表
|
|
271
|
+
const anyTimed = list.some(it => TIMED[it.state])
|
|
272
|
+
if (anyTimed) {
|
|
261
273
|
if (!tick) { paintTimer(); tick = setInterval(paintTimer, 1000) }
|
|
262
274
|
} else if (tick) { clearInterval(tick); tick = null }
|
|
263
275
|
}
|
package/overlay/main.js
CHANGED
|
@@ -7,6 +7,7 @@ const { execFile, spawn } = require('child_process')
|
|
|
7
7
|
const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
8
8
|
const STATE_FILE = path.join(DSH_HOME, 'dsh-island-state.json')
|
|
9
9
|
const POS_FILE = path.join(DSH_HOME, 'dsh-island-pos.json')
|
|
10
|
+
const CONFIG_FILE = path.join(DSH_HOME, 'dsh-island-config.json')
|
|
10
11
|
const DEAD_MS = 30000
|
|
11
12
|
const LOG_FILE = path.join(DSH_HOME, 'dsh-island-overlay.log')
|
|
12
13
|
|
|
@@ -57,6 +58,20 @@ function savePos() {
|
|
|
57
58
|
} catch (err) {}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// 拖动 = 切到「自定义位置」。宿主 watchFile 监听同一个配置文件,会立刻把新
|
|
62
|
+
// 配置下发回来(不需要浮层自己刷新界面)。写失败就算了,位置至少本次有效。
|
|
63
|
+
function persistPositionCustom() {
|
|
64
|
+
try {
|
|
65
|
+
let cur = {}
|
|
66
|
+
try { cur = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) || {} } catch (err) {}
|
|
67
|
+
if (cur.position === 'custom') return
|
|
68
|
+
cur.position = 'custom'
|
|
69
|
+
const tmp = CONFIG_FILE + '.tmp'
|
|
70
|
+
fs.writeFileSync(tmp, JSON.stringify(cur, null, 2), 'utf8')
|
|
71
|
+
fs.renameSync(tmp, CONFIG_FILE)
|
|
72
|
+
} catch (err) {}
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
// 窗口比胶囊大一圈:缩放后的胶囊要放得下,还要留出呼吸/涟漪的溢出空间
|
|
61
76
|
function winSize(s) {
|
|
62
77
|
return { w: Math.round(BASE_W * s), h: Math.round(BASE_H * s) }
|
|
@@ -113,15 +128,27 @@ function create() {
|
|
|
113
128
|
win.setIgnoreMouseEvents(true, { forward: true })
|
|
114
129
|
win.loadFile(path.join(__dirname, 'index.html'))
|
|
115
130
|
win.once('ready-to-show', () => win.showInactive())
|
|
131
|
+
// 首帧会丢:tick() 在页面还没加载完时就把状态 send 出去了(被丢弃),
|
|
132
|
+
// 而 last 已经记下,之后状态不再变化就一直不重发 —— 结果是浮层起来了却空白。
|
|
133
|
+
// 页面加载完清掉 last,让下一次 tick 无条件重发。
|
|
134
|
+
win.webContents.on('did-finish-load', () => { last = '' })
|
|
116
135
|
|
|
117
136
|
// 诊断:之前浮层会莫名其妙消失且日志里什么都没留下,这里把每种死法都记下来
|
|
118
|
-
|
|
137
|
+
let reloads = 0
|
|
138
|
+
win.webContents.on('render-process-gone', (_e, d) => {
|
|
139
|
+
log('render-process-gone: ' + JSON.stringify(d))
|
|
140
|
+
// 渲染进程挂了窗口会留在那但永远空白。重载一次;连续崩就退出,
|
|
141
|
+
// 让插件的 3 秒心跳重新拉起一个干净进程,而不是卡在空白窗口上。
|
|
142
|
+
if (reloads++ < 2) setTimeout(() => { try { win.reload() } catch (err) {} }, 1000)
|
|
143
|
+
else { log('quit: renderer keeps dying'); app.quit() }
|
|
144
|
+
})
|
|
119
145
|
win.webContents.on('unresponsive', () => log('unresponsive'))
|
|
120
146
|
win.webContents.on('responsive', () => log('responsive'))
|
|
121
147
|
app.on('child-process-gone', (_e, d) => log('child-process-gone: ' + JSON.stringify(d)))
|
|
122
148
|
win.on('hide', () => log('window hidden'))
|
|
123
149
|
win.on('show', () => log('window shown'))
|
|
124
|
-
|
|
150
|
+
// 窗口被关掉但进程还在 = 不会再有任何界面;直接退出让宿主重新拉起
|
|
151
|
+
win.on('closed', () => { log('window closed'); app.quit() })
|
|
125
152
|
|
|
126
153
|
// 别的置顶窗口偶尔会把我们压下去,定期重新抬一次
|
|
127
154
|
setInterval(() => {
|
|
@@ -276,6 +303,9 @@ ipcMain.on('drag-end', () => {
|
|
|
276
303
|
if (!dragOrigin) return
|
|
277
304
|
dragOrigin = null
|
|
278
305
|
savePos()
|
|
306
|
+
// 只写 pos.json 没用:place() 只在 position==='custom' 时才读它,
|
|
307
|
+
// 所以拖完重启又回到顶部居中。这里把配置切成 custom(宿主 watchFile 会接住)。
|
|
308
|
+
persistPositionCustom()
|
|
279
309
|
})
|
|
280
310
|
|
|
281
311
|
app.whenReady().then(() => {
|