dsh-plugin-desktop-notify 0.6.3 → 0.6.4
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 +1 -1
- package/README.md +6 -6
- package/index.js +1 -55
- package/lib/client.js +89 -139
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -44,7 +44,7 @@ Stored in localStorage under `dsh-plugin-desktop-notify.preferences`:
|
|
|
44
44
|
| `notifyError` | `true` | Notify when a turn fails |
|
|
45
45
|
| `notifyViewed` | `false` | Also notify for the session you are looking at |
|
|
46
46
|
| `badge` | `true` | Show the badge |
|
|
47
|
-
| `
|
|
47
|
+
| `orderAttention` | `true` | seat what needs you below your pins (writes the view's order, never the pin state) |
|
|
48
48
|
| `excludeSubagents` | `true` | Ignore subagent sessions |
|
|
49
49
|
| `titleComplete` / `titlePending` / `titleError` | empty | Custom titles; empty keeps the built-in copy |
|
|
50
50
|
| `maxBodyChars` | `140` | Body length limit |
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
- **等待你操作**——工具审批、提问、执行计划确认
|
|
17
17
|
- **任务失败**——这一轮出错时提示
|
|
18
18
|
- **说清楚怎么结束的**——正常完成、被拦截、输出到上限、被中止,各自一句话,不把中断说成「已完成」
|
|
19
|
-
-
|
|
19
|
+
- **需要你的排在前面**——运行中、待你审批/回答、出错、未读完成的会话,自动排到**手动置顶的下面、其余会话的上面**,处理完自动放回原位
|
|
20
20
|
- **小红点**——应用图标上显示「需要你的会话数」,你看过之后才清零
|
|
21
21
|
- **点击通知**——切到对应会话,而不是只把应用叫到前面
|
|
22
22
|
|
|
@@ -46,7 +46,7 @@ dsh-plugin-desktop-notify
|
|
|
46
46
|
| `notifyViewed` | `false` | 正在看的会话也通知(默认跳过) |
|
|
47
47
|
| `badge` | `true` | 显示小红点 |
|
|
48
48
|
| `excludeSubagents` | `true` | 忽略子智能体会话 |
|
|
49
|
-
| `
|
|
49
|
+
| `orderAttention` | `true` | 把需要你处理的会话排到手动置顶下面第一条(不改置顶状态;关掉后完全不碰排序) |
|
|
50
50
|
| `titleComplete` / `titlePending` / `titleError` | 空 | 自定义标题,空 = 用内置文案 |
|
|
51
51
|
| `maxBodyChars` | `140` | 正文长度上限 |
|
|
52
52
|
|
|
@@ -62,7 +62,7 @@ dsh-plugin-desktop-notify
|
|
|
62
62
|
__DSH_DESKTOP_NOTIFY__.capabilities() // 平台、通知权限、小红点、服务可达性
|
|
63
63
|
__DSH_DESKTOP_NOTIFY__.state() // 小红点、待处理会话、最近一次通知/点击
|
|
64
64
|
__DSH_DESKTOP_NOTIFY__.report() // 上面全部 + 窗口焦点,便于排查
|
|
65
|
-
__DSH_DESKTOP_NOTIFY__.
|
|
65
|
+
__DSH_DESKTOP_NOTIFY__.orderState() // 当前被排到前面的是哪几条、按什么顺序
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
|
|
@@ -111,9 +111,9 @@ urllib.request.urlopen(request, timeout=3).read()
|
|
|
111
111
|
## 已知限制
|
|
112
112
|
|
|
113
113
|
- 重启或刷新窗口时,已经存在的「未读 / 待审批」只恢复小红点,不再补弹横幅
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
`
|
|
114
|
+
- 这个排序是插件接管「手动排序」实现的:插件只写「需要你」那几条的顺序,其余会话仍然按最近更新往后排,
|
|
115
|
+
所以视觉上和「最近更新」一样,只是多了一条需要你的带状区域。代价是侧栏的排序方式会显示成「手动」,
|
|
116
|
+
而且你把会话拖动排序后,下一次有会话需要你时会被重排覆盖。不想要就 `orderAttention: false`
|
|
117
117
|
|
|
118
118
|
- **Windows**:点通知会切到对应会话,但窗口不一定到前台——`AppUserModelID` 和主进程 focus 通道属于桌面端,渲染层插件做不到
|
|
119
119
|
- **通知归属**:系统通知中心里显示为 DeepSeek Harness(用的是渲染层原生通知)
|
package/index.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
import { execFile, spawn } from "node:child_process"
|
|
18
18
|
import { randomBytes } from "node:crypto"
|
|
19
|
-
import { readFileSync } from "node:fs"
|
|
20
19
|
import { appendFile, mkdir, rm, writeFile } from "node:fs/promises"
|
|
21
20
|
import { homedir } from "node:os"
|
|
22
21
|
import { basename, dirname, join } from "node:path"
|
|
@@ -48,8 +47,6 @@ const DEFAULTS = {
|
|
|
48
47
|
api: true,
|
|
49
48
|
/** Where the endpoint URL is written for those tools. */
|
|
50
49
|
endpointPath: "~/.dsh/desktop-notify.endpoint",
|
|
51
|
-
/** Where the sessions this plugin pinned are remembered across window lives. */
|
|
52
|
-
pinsPath: "~/.dsh/desktop-notify-pins.json",
|
|
53
50
|
/** Longest accepted request body. */
|
|
54
51
|
apiBodyLimit: 8192,
|
|
55
52
|
/** How many unread requests the interface keeps. */
|
|
@@ -68,7 +65,6 @@ const TURN_END_BODIES = {
|
|
|
68
65
|
const API_REQUEST_PATH = "/desktop-notify/request"
|
|
69
66
|
const API_PENDING_PATH = "/desktop-notify/pending"
|
|
70
67
|
const API_TURN_PATH = "/desktop-notify/turn"
|
|
71
|
-
const API_PINS_PATH = "/desktop-notify/pins"
|
|
72
68
|
|
|
73
69
|
/** Read one JSON request body, bounded by `limit` bytes. */
|
|
74
70
|
function readBody(request, limit) {
|
|
@@ -241,37 +237,6 @@ function mountInterface(ctx, config, state) {
|
|
|
241
237
|
answer(response, 200, entry === undefined ? {} : { kind: entry.kind, at: entry.at })
|
|
242
238
|
}
|
|
243
239
|
|
|
244
|
-
/**
|
|
245
|
-
* Remember which sessions this plugin pinned. The renderer's origin (and so
|
|
246
|
-
* its localStorage) changes with every launch, while the sidebar pins live
|
|
247
|
-
* on the Host; this file is what lets a restart release the pins it owns
|
|
248
|
-
* instead of mistaking them for the user's own.
|
|
249
|
-
*/
|
|
250
|
-
const pins = (request, response) => {
|
|
251
|
-
if (rejected(request, response, false)) return
|
|
252
|
-
if (request.method === "GET") {
|
|
253
|
-
answer(response, 200, { ids: [...state.pinnedByPlugin] })
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
if (request.method !== "POST") {
|
|
257
|
-
answer(response, 405, { ok: false, error: "use GET or POST" })
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
void readBody(request, Math.max(1024, config.apiBodyLimit))
|
|
261
|
-
.then((payload) => {
|
|
262
|
-
const ids = Array.isArray(payload?.ids) ? payload.ids.filter((id) => typeof id === "string" && id !== "") : []
|
|
263
|
-
state.pinnedByPlugin = new Set(ids.slice(0, Math.max(1, config.maxTrackedSessions)))
|
|
264
|
-
const path = expandHome(config.pinsPath)
|
|
265
|
-
if (path !== "") {
|
|
266
|
-
void mkdir(dirname(path), { recursive: true })
|
|
267
|
-
.then(() => writeFile(path, `${JSON.stringify({ ids: [...state.pinnedByPlugin] })}\n`, "utf8"))
|
|
268
|
-
.catch(() => {})
|
|
269
|
-
}
|
|
270
|
-
answer(response, 200, { ok: true, tracked: state.pinnedByPlugin.size })
|
|
271
|
-
})
|
|
272
|
-
.catch((error) => answer(response, 400, { ok: false, error: String(error?.message ?? error) }))
|
|
273
|
-
}
|
|
274
|
-
|
|
275
240
|
let disposers
|
|
276
241
|
try {
|
|
277
242
|
disposers = [
|
|
@@ -292,13 +257,6 @@ function mountInterface(ctx, config, state) {
|
|
|
292
257
|
answer(response, 500, {})
|
|
293
258
|
}
|
|
294
259
|
} }),
|
|
295
|
-
server.register({ kind: "exact", path: API_PINS_PATH, handler: (request, response) => {
|
|
296
|
-
try {
|
|
297
|
-
pins(request, response)
|
|
298
|
-
} catch {
|
|
299
|
-
answer(response, 500, { ok: false })
|
|
300
|
-
}
|
|
301
|
-
} }),
|
|
302
260
|
]
|
|
303
261
|
} catch (error) {
|
|
304
262
|
void noteToLog(config.logPath, `interface: cannot register routes: ${String(error)}`)
|
|
@@ -449,19 +407,7 @@ function runDetached(command, args) {
|
|
|
449
407
|
export function apply(ctx, rawConfig) {
|
|
450
408
|
const config = resolveConfig(rawConfig)
|
|
451
409
|
/** The newest turn-end kind per session, read by the renderer over the local interface. */
|
|
452
|
-
const state = { lastTurnEnds: new Map()
|
|
453
|
-
// Seeded synchronously: the renderer asks for it as soon as it applies.
|
|
454
|
-
try {
|
|
455
|
-
const pinsPath = expandHome(config.pinsPath)
|
|
456
|
-
if (pinsPath !== "") {
|
|
457
|
-
const remembered = JSON.parse(readFileSync(pinsPath, "utf8"))
|
|
458
|
-
if (Array.isArray(remembered?.ids)) {
|
|
459
|
-
for (const id of remembered.ids) if (typeof id === "string" && id !== "") state.pinnedByPlugin.add(id)
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
} catch {
|
|
463
|
-
/* no remembered pins yet */
|
|
464
|
-
}
|
|
410
|
+
const state = { lastTurnEnds: new Map() }
|
|
465
411
|
mountInterface(ctx, config, state)
|
|
466
412
|
|
|
467
413
|
const logPath = expandHome(config.logPath)
|
package/lib/client.js
CHANGED
|
@@ -30,10 +30,8 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
factory: (require) => {
|
|
31
31
|
const NS = "dsh-plugin-desktop-notify"
|
|
32
32
|
/** Reported by capabilities(); keep in step with package.json (cross-machine support). */
|
|
33
|
-
const VERSION = "0.6.
|
|
33
|
+
const VERSION = "0.6.4"
|
|
34
34
|
const PREFERENCES_KEY = `${NS}:preferences`
|
|
35
|
-
/** Sessions this plugin pinned itself; a reload reads it back so it never drops a user's own pin. */
|
|
36
|
-
const PINNED_KEY = `${NS}:pinned`
|
|
37
35
|
|
|
38
36
|
/** Defaults; override per field with the localStorage JSON described in README. */
|
|
39
37
|
const DEFAULTS = {
|
|
@@ -44,8 +42,8 @@ window.__ModuleLoader__.load({
|
|
|
44
42
|
notifyViewed: false,
|
|
45
43
|
badge: true,
|
|
46
44
|
excludeSubagents: true,
|
|
47
|
-
//
|
|
48
|
-
|
|
45
|
+
// Seat what needs you directly below the manual pins in its group.
|
|
46
|
+
orderAttention: true,
|
|
49
47
|
// Empty means "use the built-in copy for the active UI language".
|
|
50
48
|
titleComplete: "",
|
|
51
49
|
titlePending: "",
|
|
@@ -145,8 +143,9 @@ window.__ModuleLoader__.load({
|
|
|
145
143
|
*/
|
|
146
144
|
const TURN_KIND_PATH = "/desktop-notify/turn"
|
|
147
145
|
const TURN_LOOKUP_TIMEOUT_MS = 800
|
|
148
|
-
/**
|
|
149
|
-
const
|
|
146
|
+
/** The sidebar's flat-list order key, and how long a tier change waits before it is written. */
|
|
147
|
+
const FLAT_ORDER_KEY = "__flat_session_order__"
|
|
148
|
+
const ORDER_SETTLE_MS = 600
|
|
150
149
|
|
|
151
150
|
/** Ask the Host how one turn ended; undefined when it cannot answer in time. */
|
|
152
151
|
function lookupTurnKind(sessionId) {
|
|
@@ -198,18 +197,6 @@ window.__ModuleLoader__.load({
|
|
|
198
197
|
return preferences
|
|
199
198
|
}
|
|
200
199
|
|
|
201
|
-
/** The session ids this plugin pinned in an earlier window life. */
|
|
202
|
-
function readPinnedIds() {
|
|
203
|
-
try {
|
|
204
|
-
const raw = storage()?.getItem(PINNED_KEY)
|
|
205
|
-
if (typeof raw !== "string" || raw === "") return []
|
|
206
|
-
const parsed = JSON.parse(raw)
|
|
207
|
-
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : []
|
|
208
|
-
} catch {
|
|
209
|
-
return []
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
200
|
/** Collapse whitespace and cut to the body budget. */
|
|
214
201
|
function bodyText(text, maxChars) {
|
|
215
202
|
const collapsed = String(text).replace(/\s+/gu, " ").trim()
|
|
@@ -328,128 +315,95 @@ window.__ModuleLoader__.load({
|
|
|
328
315
|
/** sessionId -> ms timestamp when the current turn was first seen running. */
|
|
329
316
|
const turnStartedAt = new Map()
|
|
330
317
|
let lastBadge
|
|
331
|
-
/** Sessions this plugin pinned itself, so unpinning never touches the user's own pins. */
|
|
332
|
-
const selfPinned = new Set(readPinnedIds())
|
|
333
|
-
/** Whether the Host has answered with its remembered pins; nothing is pinned before that. */
|
|
334
|
-
let pinsReady = false
|
|
335
|
-
/** The most recent wanted set, so hydration can reconcile the moment it lands. */
|
|
336
|
-
let lastWanted = new Set()
|
|
337
|
-
|
|
338
318
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
319
|
+
* The sessions that need you, in the order they started needing you.
|
|
320
|
+
* FIFO on purpose: a new arrival joins the bottom of the tier instead of
|
|
321
|
+
* displacing whoever was already in it, so a few running turns can never
|
|
322
|
+
* shuffle the sidebar under the cursor.
|
|
342
323
|
*/
|
|
343
|
-
|
|
324
|
+
let tier = []
|
|
325
|
+
/** One coalesced write per settled tier, never one per status tick. */
|
|
326
|
+
let tierTimer
|
|
327
|
+
/** Whether an order was ever written, so an empty tier can clear it once and then stay quiet. */
|
|
328
|
+
let hasWrittenOrder = false
|
|
329
|
+
/** The last order written per account key; an unchanged tier touches nothing. */
|
|
330
|
+
const lastOrderByKey = new Map()
|
|
331
|
+
|
|
332
|
+
/** The sidebar's workspace snapshot, or undefined on a build without the service. */
|
|
333
|
+
function workspaceSnapshot() {
|
|
344
334
|
try {
|
|
345
|
-
|
|
346
|
-
} catch {
|
|
347
|
-
/* a storage-less renderer keeps the set for this life only */
|
|
348
|
-
}
|
|
349
|
-
if (typeof fetch !== "function") return
|
|
350
|
-
try {
|
|
351
|
-
void fetch(PINS_PATH, {
|
|
352
|
-
method: "POST",
|
|
353
|
-
headers: { "content-type": "application/json" },
|
|
354
|
-
body: JSON.stringify({ ids: [...selfPinned] }),
|
|
355
|
-
}).catch(() => {})
|
|
335
|
+
return ctx.uiWorkspace?.workspaces?.list?.getSnapshot?.()
|
|
356
336
|
} catch {
|
|
357
|
-
|
|
337
|
+
return undefined
|
|
358
338
|
}
|
|
359
339
|
}
|
|
360
340
|
|
|
361
|
-
/**
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
async function hydratePins() {
|
|
367
|
-
if (typeof fetch !== "function") {
|
|
368
|
-
pinsReady = true
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
try {
|
|
372
|
-
const response = await fetch(PINS_PATH, { headers: { accept: "application/json" }, cache: "no-store" })
|
|
373
|
-
if (response.ok) {
|
|
374
|
-
const payload = await response.json()
|
|
375
|
-
if (Array.isArray(payload?.ids)) {
|
|
376
|
-
for (const sessionId of payload.ids) if (typeof sessionId === "string" && sessionId !== "") selfPinned.add(sessionId)
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
} catch {
|
|
380
|
-
/* no Host half: this life reconciles from localStorage alone */
|
|
381
|
-
}
|
|
382
|
-
pinsReady = true
|
|
383
|
-
reconcilePins(lastWanted)
|
|
341
|
+
/** True when two id arrays are identical, so a no-op sync never reorders anything. */
|
|
342
|
+
function sameOrder(left, right) {
|
|
343
|
+
if (left.length !== right.length) return false
|
|
344
|
+
for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false
|
|
345
|
+
return true
|
|
384
346
|
}
|
|
385
347
|
|
|
386
|
-
/**
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
348
|
+
/** Recompute the tier; only a membership change schedules a write. */
|
|
349
|
+
function updateTier(wanted) {
|
|
350
|
+
const next = tier.filter((sessionId) => wanted.has(sessionId))
|
|
351
|
+
for (const sessionId of wanted) if (!next.includes(sessionId)) next.push(sessionId)
|
|
352
|
+
if (sameOrder(next, tier)) return
|
|
353
|
+
tier = next
|
|
354
|
+
scheduleOrderWrite()
|
|
393
355
|
}
|
|
394
356
|
|
|
395
|
-
/**
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (pending !== null && typeof pending === "object" && typeof pending.catch === "function") pending.catch(() => {})
|
|
403
|
-
} catch {
|
|
404
|
-
/* the view keeps its order */
|
|
405
|
-
}
|
|
357
|
+
/** Coalesce a burst of status ticks into one write. */
|
|
358
|
+
function scheduleOrderWrite() {
|
|
359
|
+
if (tierTimer !== undefined) return
|
|
360
|
+
tierTimer = setTimeout(() => {
|
|
361
|
+
tierTimer = undefined
|
|
362
|
+
writeOrder()
|
|
363
|
+
}, ORDER_SETTLE_MS)
|
|
406
364
|
}
|
|
407
365
|
|
|
408
366
|
/**
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* the
|
|
412
|
-
* never a session the user pinned by hand, and never one this plugin did
|
|
413
|
-
* not pin in the first place.
|
|
367
|
+
* Seat the tier directly below the manual pins in every group. Only the
|
|
368
|
+
* tier ids are written, so everything else keeps falling back to recency:
|
|
369
|
+
* the result reads like the "most recent" sort with a needs-you band.
|
|
414
370
|
*/
|
|
415
|
-
function
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
selfPinned.delete(sessionId)
|
|
422
|
-
callPin(sessionId, false)
|
|
423
|
-
changed = true
|
|
424
|
-
}
|
|
425
|
-
if (changed) persistPins()
|
|
426
|
-
return
|
|
427
|
-
}
|
|
428
|
-
const snapshot = pinnedSnapshot()
|
|
429
|
-
if (snapshot === undefined) return
|
|
430
|
-
const pinned = new Set(Array.isArray(snapshot.pinnedSessionIds) ? snapshot.pinnedSessionIds : [])
|
|
431
|
-
// An archived session is out of the tree anyway, and pinning it is not
|
|
432
|
-
// this plugin's business.
|
|
371
|
+
function writeOrder() {
|
|
372
|
+
const view = ctx.uiWorkspace?.view
|
|
373
|
+
if (typeof view?.setSessionOrder !== "function") return
|
|
374
|
+
const snapshot = workspaceSnapshot()
|
|
375
|
+
const list = ctx.sessions.list.getSnapshot()
|
|
376
|
+
if (snapshot === undefined || list?.phase !== "ready") return
|
|
433
377
|
const archived = new Set(Array.isArray(snapshot.archivedSessionIds) ? snapshot.archivedSessionIds : [])
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
378
|
+
const live = tier.filter((sessionId) => !archived.has(sessionId))
|
|
379
|
+
const active = preferences.orderAttention && live.length > 0
|
|
380
|
+
if (!active && !hasWrittenOrder) return
|
|
381
|
+
const accounted = new Set()
|
|
382
|
+
const groups = []
|
|
383
|
+
for (const item of Array.isArray(snapshot.items) ? snapshot.items : []) {
|
|
384
|
+
const memberIds = Array.isArray(item?.sessionIds) ? item.sessionIds : []
|
|
385
|
+
for (const sessionId of memberIds) accounted.add(sessionId)
|
|
386
|
+
groups.push([item?.workspaceId ?? "", memberIds])
|
|
387
|
+
}
|
|
388
|
+
const ids = Array.isArray(list.ids) ? list.ids : []
|
|
389
|
+
const known = (sessionId) => list.byId?.[sessionId] !== undefined
|
|
390
|
+
groups.push(["", ids.filter((sessionId) => known(sessionId) && !accounted.has(sessionId))])
|
|
391
|
+
groups.push([FLAT_ORDER_KEY, ids.filter(known)])
|
|
392
|
+
for (const [key, memberIds] of groups) {
|
|
393
|
+
const members = new Set(memberIds)
|
|
394
|
+
const order = active ? live.filter((sessionId) => members.has(sessionId)) : []
|
|
395
|
+
const signature = order.join("\n")
|
|
396
|
+
if (lastOrderByKey.get(key) === signature) continue
|
|
397
|
+
try {
|
|
398
|
+
view.setSessionOrder(key, order, {})
|
|
399
|
+
lastOrderByKey.set(key, signature)
|
|
400
|
+
} catch {
|
|
401
|
+
/* a view store that refuses the write keeps its own order */
|
|
402
|
+
}
|
|
446
403
|
}
|
|
447
|
-
|
|
404
|
+
hasWrittenOrder = active
|
|
448
405
|
}
|
|
449
406
|
|
|
450
|
-
// Ask the Host which pins an earlier launch left; nothing is pinned until it answers.
|
|
451
|
-
void hydratePins()
|
|
452
|
-
|
|
453
407
|
/** The last banner attempt and its outcome — Windows has several ways to swallow it. */
|
|
454
408
|
let lastNotify
|
|
455
409
|
/** The last banner click and what it managed to do — Windows/macOS differ here. */
|
|
@@ -762,7 +716,7 @@ window.__ModuleLoader__.load({
|
|
|
762
716
|
})
|
|
763
717
|
}
|
|
764
718
|
|
|
765
|
-
// The
|
|
719
|
+
// The tier follows the same facts the banner and badge use, plus a
|
|
766
720
|
// running turn: anything that would make you look belongs on top.
|
|
767
721
|
const current = attention.get(sessionId) ?? {}
|
|
768
722
|
if (!looking && (running || current.completed === true || current.pendingKind !== undefined || current.error !== undefined)) {
|
|
@@ -777,7 +731,7 @@ window.__ModuleLoader__.load({
|
|
|
777
731
|
previous.delete(sessionId)
|
|
778
732
|
turnStartedAt.delete(sessionId)
|
|
779
733
|
}
|
|
780
|
-
|
|
734
|
+
updateTier(wanted)
|
|
781
735
|
applyBadge()
|
|
782
736
|
}
|
|
783
737
|
|
|
@@ -912,28 +866,24 @@ window.__ModuleLoader__.load({
|
|
|
912
866
|
badge: lastBadge ?? 0,
|
|
913
867
|
attention: [...attention.entries()].map(([sessionId, entry]) => ({ sessionId, ...entry })),
|
|
914
868
|
tracked: [...previous.keys()],
|
|
915
|
-
|
|
869
|
+
order: [...tier],
|
|
916
870
|
lastNotify: lastNotify ?? null,
|
|
917
871
|
lastClick: lastClick ?? null,
|
|
918
872
|
}
|
|
919
873
|
},
|
|
920
|
-
/**
|
|
921
|
-
|
|
922
|
-
const snapshot = pinnedSnapshot()
|
|
874
|
+
/** Which sessions the plugin seats under the pins, and in what order. */
|
|
875
|
+
orderState() {
|
|
923
876
|
return {
|
|
924
|
-
enabled: preferences.
|
|
925
|
-
|
|
926
|
-
|
|
877
|
+
enabled: preferences.orderAttention,
|
|
878
|
+
tier: [...tier],
|
|
879
|
+
written: Object.fromEntries([...lastOrderByKey].map(([key, value]) => [key, value === "" ? 0 : value.split("\n").length])),
|
|
927
880
|
}
|
|
928
881
|
},
|
|
929
|
-
/**
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
935
|
-
persistPins()
|
|
936
|
-
return this.pinState()
|
|
882
|
+
/** Drop the whole tier; the next settle clears it from the sidebar. */
|
|
883
|
+
clearOrder() {
|
|
884
|
+
tier = []
|
|
885
|
+
scheduleOrderWrite()
|
|
886
|
+
return this.orderState()
|
|
937
887
|
},
|
|
938
888
|
simulateComplete(sessionId = "desktop-notify-simulation") {
|
|
939
889
|
const table = copy(languageOf())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-desktop-notify",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Desktop notifications and a Dock/taskbar badge for DeepSeek Harness: a finished turn, a pending approval or question, and a failure, with click-through back to the session",
|
|
6
6
|
"license": "MIT",
|