@cowwo/dsh-icon-custom 0.16.0 → 0.19.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.
package/lib/index.js CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  normalizeUnreadConfig,
41
41
  pruneSeen
42
42
  } from './unread.js'
43
+ import { normalizeRunningConfig } from './running.js'
43
44
 
44
45
  const MAX_BYTES = 10 * 1024 * 1024 // 10 MB
45
46
  // Normalized mime -> safe extension. CUR (mouse cursor) is accepted as ICO.
@@ -338,6 +339,11 @@ export class FaviconCustomService extends TypertRemoteService {
338
339
  // survives a cleared browser profile and means the same thing on every
339
340
  // device, while each browser keeps its own copy as cache + offline queue.
340
341
  this.seenPath = join(this.storageDir, 'unread-seen.json')
342
+ // The "show a yellow mark for every session that is running" switch. Its own
343
+ // file and its own endpoint pair: the running FACT is official (the browser
344
+ // list already carries each session's `running`), so nothing about it is
345
+ // stored here — only whether to show the projection of it.
346
+ this.runningPath = join(this.storageDir, 'running.json')
341
347
  // { activeId, icons: { [id]: { id, mime, ext, name, size, savedAt, rev, pwa, logo } } }
342
348
  this.library = { activeId: null, icons: {} }
343
349
  // The active icon's loaded state: { rev, mime, ext, name, size, savedAt, pwa, logo, bytes } | null
@@ -346,12 +352,16 @@ export class FaviconCustomService extends TypertRemoteService {
346
352
  // Kept in its own file so a settings change can never corrupt the icon
347
353
  // library index.
348
354
  this.unread = normalizeUnreadConfig(null)
355
+ // Running-mark switch: whether the yellow projection of the official
356
+ // `running` fact is drawn. See lib/running.js.
357
+ this.running = normalizeRunningConfig(null)
349
358
  // Host-side watermark: { [sessionId]: endAt } — see lib/unread.js.
350
359
  this.seen = {}
351
360
  // Platform PWA manifest (raw + parsed), read once at startup.
352
361
  this.baseManifest = loadBaseManifest()
353
362
  this._load()
354
363
  this._loadUnread()
364
+ this._loadRunning()
355
365
  this._loadUnreadSeen()
356
366
  // "How did this session's last turn end" as an official session
357
367
  // projection. Optional registration on purpose: this plugin is a display
@@ -450,6 +460,19 @@ export class FaviconCustomService extends TypertRemoteService {
450
460
  return writeFile(this.unreadPath, JSON.stringify(this.unread, null, 2))
451
461
  }
452
462
 
463
+ /** Load the running-mark switch (missing or corrupt file = default: on). */
464
+ _loadRunning() {
465
+ try {
466
+ const raw = JSON.parse(readFileSync(this.runningPath).toString('utf8'))
467
+ this.running = normalizeRunningConfig(raw)
468
+ } catch { /* no persisted switch yet: keep the default */ }
469
+ }
470
+
471
+ /** Persist the running-mark switch (best effort, advisory file). */
472
+ _saveRunning() {
473
+ return writeFile(this.runningPath, JSON.stringify(this.running, null, 2))
474
+ }
475
+
453
476
  /**
454
477
  * Load the host-side watermark.
455
478
  *
@@ -516,6 +539,35 @@ export class FaviconCustomService extends TypertRemoteService {
516
539
  return this._unreadRuleView()
517
540
  }
518
541
 
542
+ /**
543
+ * typert endpoint: read the running-mark switch.
544
+ *
545
+ * Its own endpoint pair, not a field on the unread rule: the two features are
546
+ * independent (a red dot says "something ended and you have not looked", a
547
+ * yellow mark says "something is running right now"), and neither switch may
548
+ * drag the other's storage or response shape along. There is deliberately no
549
+ * `running` field here — who is running is the official host's answer and
550
+ * travels in the ordinary session list.
551
+ * @returns `{ enabled }`.
552
+ */
553
+ async getRunningRule() {
554
+ return { ...this.running }
555
+ }
556
+
557
+ /**
558
+ * typert endpoint: flip the running-mark switch.
559
+ * @param request - `{ enabled: boolean }`; anything else falls back to the default.
560
+ * @returns the stored switch.
561
+ */
562
+ async setRunningRule(request) {
563
+ if (request === null || typeof request !== 'object') {
564
+ throw new Error('缺少 enabled 参数')
565
+ }
566
+ this.running = normalizeRunningConfig(request)
567
+ await this._saveRunning().catch(() => {}) // switch file is advisory
568
+ return { ...this.running }
569
+ }
570
+
519
571
  /**
520
572
  * typert endpoint: merge this browser's "seen" watermarks into the Host's copy.
521
573
  *
package/lib/running.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * dsh-icon-custom · 「进行中黄点」的开关(宿主半,纯函数)
3
+ *
4
+ * 这里**没有**任何"谁在跑"的事实,一条也没有——那是官方的:
5
+ * 宿主 `ctx.agents.get(id)?.status === 'running'` 经 `api-session/status`
6
+ * 事件实时推到浏览器,客户端会话列表里本来就带着每个会话的 `running`。
7
+ * 这个插件只把那件事**多投影几次**(黄点/黄数字/黄 favicon 角标)。
8
+ *
9
+ * 所以这个文件只有一个布尔开关,和一条纪律:
10
+ * **不落盘"进行中",只落盘"要不要显示"。** 进程重启后冷会话按官方口径
11
+ * 永远是 `running:false`,不会留下一个永远亮着的假黄点;而开关是偏好,
12
+ * 应该跨设备、跨重启跟着你,所以它归宿主,和红点规则并列但互不牵连
13
+ * (见 docs/adr/0010-running-mark-is-a-second-projection.md)。
14
+ */
15
+
16
+ /** 默认:显示黄点。 */
17
+ export const RUNNING_MARK_DEFAULT = true
18
+
19
+ /**
20
+ * 把任意输入夹成 `{ enabled }`。
21
+ *
22
+ * 只认严格的 `true`/`false`;缺项、脏值、null 一律回落默认——一个读不懂的
23
+ * 开关值不该把一整套标记的可见性变成猜谜。
24
+ * @param input - `{ enabled? }`,可以是脏的、缺项的、null
25
+ * @returns `{ enabled }` —— 永远完整
26
+ */
27
+ export function normalizeRunningConfig(input) {
28
+ const source = input !== null && typeof input === 'object' ? input : {}
29
+ return {
30
+ enabled: typeof source.enabled === 'boolean' ? source.enabled : RUNNING_MARK_DEFAULT
31
+ }
32
+ }
@@ -57,6 +57,8 @@ const unreadRuleSchema = z.object({
57
57
  appBadge: z.boolean(),
58
58
  /** 进入会话后要连续待满多少秒才算"看过";0 = 进入即已读。上限 10 分钟。 */
59
59
  clearDelaySec: z.number().int().nonnegative().max(600),
60
+ /** 实验项:点清单条目时在侧栏定位到该会话(展开工作区 + 滚动 + 闪一下;贴产品 DOM,可一键关闭)。 */
61
+ revealOnOpen: z.boolean(),
60
62
  /**
61
63
  * 宿主此刻的 `Date.now()`。
62
64
  * 客户端用它把"看过"水位换算进宿主时钟域(`turn/end.time` 也是宿主时钟):
@@ -77,7 +79,8 @@ const setUnreadRuleRequestSchema = z.object({
77
79
  pending: z.boolean().optional(),
78
80
  workspaceDot: z.boolean().optional(),
79
81
  appBadge: z.boolean().optional(),
80
- clearDelaySec: z.number().int().nonnegative().max(600).optional()
82
+ clearDelaySec: z.number().int().nonnegative().max(600).optional(),
83
+ revealOnOpen: z.boolean().optional()
81
84
  })
82
85
 
83
86
  /** 一次水位线同步:浏览器把自己那份交上来,宿主逐会话取 max 后回权威表。 */
@@ -89,6 +92,21 @@ const unreadSeenSchema = z.object({
89
92
  seen: z.record(z.string(), z.number())
90
93
  })
91
94
 
95
+ /**
96
+ * 进行中黄点的开关。
97
+ *
98
+ * 这里**没有**"谁在跑"的字段,一条都没有:那是官方的 `SessionSummary.running`,
99
+ * 由官方宿主实时推给浏览器,插件只读不存。宿主这一侧只有"要不要显示"这一个偏好,
100
+ * 和红点规则并列、互不牵连(自己的端点对、自己的文件)。
101
+ */
102
+ const runningRuleSchema = z.object({
103
+ enabled: z.boolean()
104
+ })
105
+
106
+ const setRunningRuleRequestSchema = z.object({
107
+ enabled: z.boolean().optional()
108
+ })
109
+
92
110
  const setIconResultSchema = statusSchema
93
111
 
94
112
  export const TYPERT = {
@@ -238,6 +256,45 @@ export const TYPERT = {
238
256
  },
239
257
  sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
240
258
  },
259
+ {
260
+ id: 'dsh-icon-custom#iconCustom/getRunningRule',
261
+ service: 'iconCustom',
262
+ namespace: 'iconCustom',
263
+ method: 'getRunningRule',
264
+ invocation: { kind: 'direct' },
265
+ parameters: [],
266
+ result: {
267
+ mode: 'strict',
268
+ typeSymbol: 'dsh-icon-custom#iconCustom/getRunningRule:result',
269
+ create: () => runningRuleSchema
270
+ },
271
+ sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
272
+ },
273
+ {
274
+ id: 'dsh-icon-custom#iconCustom/setRunningRule',
275
+ service: 'iconCustom',
276
+ namespace: 'iconCustom',
277
+ method: 'setRunningRule',
278
+ invocation: { kind: 'direct' },
279
+ parameters: [
280
+ {
281
+ name: 'request',
282
+ wire: 'request',
283
+ source: 'json',
284
+ codec: {
285
+ mode: 'strict',
286
+ typeSymbol: 'dsh-icon-custom#iconCustom/setRunningRule:request',
287
+ create: () => setRunningRuleRequestSchema
288
+ }
289
+ }
290
+ ],
291
+ result: {
292
+ mode: 'strict',
293
+ typeSymbol: 'dsh-icon-custom#iconCustom/setRunningRule:result',
294
+ create: () => runningRuleSchema
295
+ },
296
+ sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
297
+ },
241
298
  {
242
299
  id: 'dsh-icon-custom#iconCustom/setUnreadSeen',
243
300
  service: 'iconCustom',
@@ -322,7 +379,7 @@ export const TYPERT = {
322
379
  {
323
380
  kind: 'method',
324
381
  name: 'getUnreadRule',
325
- signature: 'async getUnreadRule(): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; hostNow: number; seen: Record<string, number> }>',
382
+ signature: 'async getUnreadRule(): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; revealOnOpen: boolean; hostNow: number; seen: Record<string, number> }>',
326
383
  description: "Read the unread-badge rule, the Host clock and the seen watermark.",
327
384
  summary: "Read the unread-badge rule.",
328
385
  jsDoc: "/** Read the unread-badge rule. */",
@@ -331,7 +388,7 @@ export const TYPERT = {
331
388
  {
332
389
  kind: 'method',
333
390
  name: 'setUnreadRule',
334
- signature: 'async setUnreadRule(request: { reasons: Record<string, boolean>; pending?: boolean; workspaceDot?: boolean; appBadge?: boolean; clearDelaySec?: number }): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; hostNow: number; seen: Record<string, number> }>',
391
+ signature: 'async setUnreadRule(request: { reasons: Record<string, boolean>; pending?: boolean; workspaceDot?: boolean; appBadge?: boolean; clearDelaySec?: number; revealOnOpen?: boolean }): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; revealOnOpen: boolean; hostNow: number; seen: Record<string, number> }>',
335
392
  description: "Replace the unread-badge rule: which turn-end reasons light the red dot.",
336
393
  summary: "Set the unread-badge rule.",
337
394
  jsDoc: "/** Set the unread-badge rule. */",
@@ -345,6 +402,24 @@ export const TYPERT = {
345
402
  summary: "Merge the seen watermark.",
346
403
  jsDoc: "/** Merge the seen watermark. */",
347
404
  tags: []
405
+ },
406
+ {
407
+ kind: 'method',
408
+ name: 'getRunningRule',
409
+ signature: 'async getRunningRule(): Promise<{ enabled: boolean }>',
410
+ description: "Read the running-mark switch (yellow mark for sessions that are running right now).",
411
+ summary: "Read the running-mark switch.",
412
+ jsDoc: "/** Read the running-mark switch. */",
413
+ tags: []
414
+ },
415
+ {
416
+ kind: 'method',
417
+ name: 'setRunningRule',
418
+ signature: 'async setRunningRule(request: { enabled?: boolean }): Promise<{ enabled: boolean }>',
419
+ description: "Flip the running-mark switch. The running fact itself is official and is never stored here.",
420
+ summary: "Set the running-mark switch.",
421
+ jsDoc: "/** Set the running-mark switch. */",
422
+ tags: []
348
423
  }
349
424
  ],
350
425
  types: []
package/lib/unread.js CHANGED
@@ -46,12 +46,25 @@ export const UNREAD_PENDING_DEFAULT = true
46
46
  /**
47
47
  * 默认:在工作区那一行的文件夹图标上画红点。
48
48
  *
49
- * 这是全套功能里**唯一贴着产品 DOM 走**的部分(侧栏那片没有插槽),
49
+ * 这是全套功能里**贴着产品 DOM 走**的两处之一(另一处见下面的 `UNREAD_REVEAL_DEFAULT`),
50
50
  * 所以它有自己的开关:一旦 DSH 升级改了结构导致它失效或画歪,
51
51
  * 关掉它就行,红点数字/清单/跳转那些稳的部分完全不受影响。
52
52
  */
53
53
  export const UNREAD_WORKSPACE_DOT_DEFAULT = true
54
54
 
55
+ /**
56
+ * 默认:点清单条目时,顺带在侧栏定位到那个会话(展开它所在的工作区、滚到那一行并闪一下)。
57
+ *
58
+ * 这是第二处贴着产品 DOM 走的部分。官方的"展开 + 滚动"只服务于它自己的搜索结果
59
+ * (内部状态,没有对外的 Service),所以插件只能**模拟点击**官方的工作区行与
60
+ * "显示更多"按钮。DSH 升级改结构时它会失效,失效的表现只有一个:
61
+ * **点进去照常打开会话,只是侧栏不动**。不想让它动侧栏就关掉这一项。
62
+ *
63
+ * 还有一件事要说清:展开是**官方自己记住**的状态(存在浏览器里),不会自己收回去 ——
64
+ * 官方搜索跳转也是这个行为。
65
+ */
66
+ export const UNREAD_REVEAL_DEFAULT = true
67
+
55
68
  /**
56
69
  * 默认:在系统应用图标上显示角标(安装为应用后生效)。
57
70
  *
@@ -97,8 +110,8 @@ export function normalizeReasons(input) {
97
110
 
98
111
  /**
99
112
  * 把任意输入夹成完整设置。
100
- * @param input - `{ reasons?, pending?, workspaceDot?, appBadge?, clearDelaySec? }`,可以是脏的、缺项的、null
101
- * @returns `{ reasons, pending, workspaceDot, appBadge, clearDelaySec }` —— 每一项都完整
113
+ * @param input - `{ reasons?, pending?, workspaceDot?, appBadge?, clearDelaySec?, revealOnOpen? }`,可以是脏的、缺项的、null
114
+ * @returns `{ reasons, pending, workspaceDot, appBadge, clearDelaySec, revealOnOpen }` —— 每一项都完整
102
115
  */
103
116
  export function normalizeUnreadConfig(input) {
104
117
  const source = input !== null && typeof input === 'object' ? input : {}
@@ -107,7 +120,8 @@ export function normalizeUnreadConfig(input) {
107
120
  pending: typeof source.pending === 'boolean' ? source.pending : UNREAD_PENDING_DEFAULT,
108
121
  workspaceDot: typeof source.workspaceDot === 'boolean' ? source.workspaceDot : UNREAD_WORKSPACE_DOT_DEFAULT,
109
122
  appBadge: typeof source.appBadge === 'boolean' ? source.appBadge : UNREAD_APP_BADGE_DEFAULT,
110
- clearDelaySec: normalizeClearDelay(source.clearDelaySec)
123
+ clearDelaySec: normalizeClearDelay(source.clearDelaySec),
124
+ revealOnOpen: typeof source.revealOnOpen === 'boolean' ? source.revealOnOpen : UNREAD_REVEAL_DEFAULT
111
125
  }
112
126
  }
113
127
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cowwo/dsh-icon-custom",
3
- "version": "0.16.0",
4
- "description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存,自动适配安卓/iOS/平板 PWA 图标,并可把未读数字显示为系统应用图标角标(Windows 任务栏)。· Custom browser tab favicon: upload SVG/PNG/ICO from the settings page, applies instantly and persists, auto-sizes PWA icons for Android/iOS/tablets, and projects the unread count onto the system app-icon badge (Windows taskbar).",
3
+ "version": "0.19.0",
4
+ "description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存,自动适配安卓/iOS/平板 PWA 图标,显示未读红点与进行中黄点(工作区数字/会话行/左上角/标签页,数字可点开清单、点条目定位到侧栏),并可把未读数显示为系统应用图标角标(Windows 任务栏 / macOS Dock,含通知权限申请)。· Custom browser tab favicon: upload SVG/PNG/ICO from the settings page, applies instantly and persists, auto-sizes PWA icons for Android/iOS/tablets, shows unread red dots and running yellow marks (workspace number / session row / top-left / tab icon, both counts open a list whose rows locate the session in the sidebar), and projects the unread count onto the system app-icon badge (Windows taskbar / macOS Dock, notifications permission included).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "license": "MIT",
8
8
  "scripts": {
9
- "test": "node test/app-badge.mjs && node test/unread-panel.mjs && node test/unread-clear.mjs && node test/unread-rows.mjs && node test/unread-seen.mjs && node test/unread-host-sync.mjs"
9
+ "test": "node test/app-badge.mjs && node test/notification-permission.mjs && node test/unread-panel.mjs && node test/unread-clear.mjs && node test/unread-rows.mjs && node test/unread-seen.mjs && node test/unread-host-sync.mjs && node test/running-mark.mjs && node test/running-panel.mjs && node test/reveal.mjs"
10
10
  },
11
11
  "dependencies": {
12
12
  "jimp": "^1.6.1",