opencode-dock-badge 0.0.0-stage → 1.0.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/README.md CHANGED
@@ -1,3 +1,67 @@
1
- # Temporary Holding Version
1
+ # opencode-dock-badge
2
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.
3
+ 给 **OpenCode Desktop** 加 macOS Dock 角标:有会话「需要你处理」或「有新回复未看」时,Dock 图标上显示红色数字。
4
+
5
+ ## 为什么需要自己写
6
+
7
+ 社区现有的 opencode 通知插件(`kdco/notify`、`opencode-notify`、`opencode-alert`、`opencode-bell` 等)**全部是给终端 TUI 版做的**,
8
+ 它们的 “Dock badge” 靠向 TTY 写 BEL 字符 `\x07` 实现。OpenCode Desktop 是 Electron web view,没有终端,收不到。
9
+
10
+ Desktop 自带的 `设置 → General → 通知` 也有两个限制:
11
+ - 只有**窗口失焦**时才弹(`getWindowFocused()` 为真直接 return)
12
+ - 只对**已打开标签页**的会话生效,后台/未打开的会话完全静默
13
+
14
+ ## 实现方式
15
+
16
+ 用 OpenCode 2.2.0+ 的实验性 GUI 插件能力(`./gui-experimental`),在 Electron 主进程里调用
17
+ `app.dock.setBadge()`,在窗口端读 OpenCode 自己维护的会话状态标记:
18
+
19
+ ```
20
+ [data-slot='titlebar-tab-item'][data-status]
21
+ ```
22
+
23
+ | data-status | 含义 | 计入角标 |
24
+ |---|---|---|
25
+ | `attention` | 需要你处理(黄点) | ✅ |
26
+ | `unread` | 有新回复未看(蓝点) | ✅ |
27
+ | `busy` | 正在运行 | ❌ |
28
+
29
+ **关键点**:这些标记由 OpenCode 自己维护,**只有你点开会话并看过才会被清掉**。
30
+ 所以角标天然满足「点掉才减少、全看完才消失」,不需要自己记状态,也永远不会和侧栏对不上。
31
+
32
+ ## 文件
33
+
34
+ | 文件 | 作用 |
35
+ |---|---|
36
+ | `src/gui.js` | 窗口侧扫 `data-status` + 主进程侧 `app.dock.setBadge` |
37
+ | `src/contract.js` | 窗口 ↔ 主进程的 Ipc 桥(`opencode-dock-badge.badge`) |
38
+ | `src/index.js` | 服务端占位部分(只为让插件有正常 id) |
39
+ | `dist/gui-experimental.cjs` | 构建产物(Vite 打包) |
40
+
41
+ ## 构建
42
+
43
+ ```bash
44
+ cd ~/.config/opencode/plugins/opencode-dock-badge
45
+ npx vite build
46
+ ```
47
+
48
+ 改完 `src/` 重新 build,桌面端会自动重载 GUI,**不用重启 app**。
49
+
50
+ ## 安装(已完成)
51
+
52
+ `~/.config/opencode/opencode.json` 的 `plugins` 里:
53
+
54
+ ```json
55
+ "plugins": [
56
+ "file:///Users/wuwu/.config/opencode/plugins/opencode-task-checker",
57
+ "file:///Users/wuwu/.config/opencode/plugins/opencode-dock-badge"
58
+ ]
59
+ ```
60
+
61
+ 首次加入时需要重启服务(`opencode-cli service restart`),之后改代码不用。
62
+
63
+ ## 注意
64
+
65
+ - GUI 插件 API 仍是 **experimental**,OpenCode 升级后可能失效,升级后先看角标还灵不灵。
66
+ - 依赖 `data-status` / `data-slot="titlebar-tab-item"` 这两个 DOM 属性,属于非公开接口,官方改结构就需要跟着改。
67
+ - 仅 macOS 生效(`app.dock.setBadge`)。
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const i=require("@opencode/plugin/gui"),g=require("effect"),l=i.Ipc.define({id:"opencode-dock-badge.badge",methods:{set:{input:g.Schema.Number},clear:{}}}),m=i.Plugin.define({i18n:{en:{"pill.pending":"needs you","command.clear":"Clear Dock badge"},zh:{"pill.pending":"待处理","command.clear":"清除 Dock 角标"}},setup(e){const r=new Set(["attention","unread"]);let t=0;const n=()=>{try{return e.main(l)}catch{return}},d=a=>{a!==t&&(t=a,n()?.set(a)?.catch(()=>{}))},o=()=>{const a=document.querySelectorAll("[data-slot='titlebar-tab-item'][data-status]");let c=0;for(const p of a)r.has(p.getAttribute("data-status"))&&(c+=1);d(c)};o();const s=setInterval(o,1500),u=()=>d(0);return e.add(i.TitlebarItem,()=>t>0?{id:"pending",label:`${t} ${e.t("pill.pending")}`,icon:"notifications",title:e.t("pill.pending")}:void 0),e.add(i.Command,()=>({id:"clear",title:e.t("command.clear"),run:u})),()=>{clearInterval(s),n()?.clear()?.catch(()=>{})}},main(e){const{app:r}=require("electron"),t=n=>{if(process.platform==="darwin")try{r.dock?.setBadge(n>0?String(n):"")}catch{}};return e.provide(l,{set:n=>t(Number(n)||0),clear:()=>t(0)}),e.log.write("info","opencode-dock-badge main ready"),()=>t(0)}});exports.default=m;
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "opencode-dock-badge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "macOS Dock badge for OpenCode Desktop — shows how many sessions need your attention or have unread replies. Count clears only when you actually open each session.",
5
+ "license": "MIT",
6
+ "author": "wuwu",
7
+ "homepage": "https://github.com/changjianghome/opencode-dock-badge#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/changjianghome/opencode-dock-badge.git"
11
+ },
12
+ "keywords": [
13
+ "opencode",
14
+ "opencode-plugin",
15
+ "opencode-desktop",
16
+ "dock-badge",
17
+ "notification",
18
+ "macos"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": "./src/index.js",
23
+ "./gui-experimental": "./dist/gui-experimental.cjs"
24
+ },
25
+ "files": [
26
+ "src",
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "build": "vite build"
32
+ },
33
+ "dependencies": {
34
+ "@opencode/plugin": "2.2.5"
35
+ },
36
+ "devDependencies": {
37
+ "vite": "^7.0.0",
38
+ "vite-plugin-solid": "^2.11.0",
39
+ "solid-js": "^1.9.0"
40
+ }
41
+ }
@@ -0,0 +1,15 @@
1
+ import { Schema } from "effect"
2
+ import { Ipc } from "@opencode/plugin/gui"
3
+
4
+ /**
5
+ * Bridge from the app window (which sees events and focus state) to the Electron main
6
+ * process (which alone can call `app.dock.setBadge`).
7
+ */
8
+ export const Badge = Ipc.define({
9
+ id: "opencode-dock-badge.badge",
10
+ methods: {
11
+ /** Paint the badge with this count; 0 clears it. */
12
+ set: { input: Schema.Number },
13
+ clear: {},
14
+ },
15
+ })
package/src/gui.js ADDED
@@ -0,0 +1,85 @@
1
+ import { Command, Plugin, TitlebarItem } from "@opencode/plugin/gui"
2
+ import { Badge } from "./contract"
3
+
4
+ /**
5
+ * Dock badge for OpenCode Desktop.
6
+ *
7
+ * Reads OpenCode's own per-session status markers from the shell tabstrip
8
+ * (`data-status`: busy / attention / unread) and mirrors the attention-worthy
9
+ * ones onto the macOS Dock badge. Because the app clears those markers only
10
+ * when the user actually opens a session, the badge goes down on its own —
11
+ * no extra bookkeeping, and it can never drift from what the UI shows.
12
+ */
13
+ export default Plugin.define({
14
+ i18n: {
15
+ en: { "pill.pending": "needs you", "command.clear": "Clear Dock badge" },
16
+ zh: { "pill.pending": "待处理", "command.clear": "清除 Dock 角标" },
17
+ },
18
+ setup(ctx) {
19
+ const COUNTED = new Set(["attention", "unread"])
20
+
21
+ let pending = 0
22
+ const bridge = () => {
23
+ try {
24
+ return ctx.main(Badge)
25
+ } catch {
26
+ return undefined
27
+ }
28
+ }
29
+ const paint = (n) => {
30
+ if (n === pending) return
31
+ pending = n
32
+ void bridge()?.set(n)?.catch(() => {})
33
+ }
34
+
35
+ const scan = () => {
36
+ const els = document.querySelectorAll("[data-slot='titlebar-tab-item'][data-status]")
37
+ let n = 0
38
+ for (const el of els) {
39
+ if (COUNTED.has(el.getAttribute("data-status"))) n += 1
40
+ }
41
+ paint(n)
42
+ }
43
+
44
+ scan()
45
+ const timer = setInterval(scan, 1500)
46
+ const clear = () => paint(0)
47
+
48
+ ctx.add(TitlebarItem, () =>
49
+ pending > 0
50
+ ? {
51
+ id: "pending",
52
+ label: `${pending} ${ctx.t("pill.pending")}`,
53
+ icon: "notifications",
54
+ title: ctx.t("pill.pending"),
55
+ }
56
+ : undefined,
57
+ )
58
+ ctx.add(Command, () => ({
59
+ id: "clear",
60
+ title: ctx.t("command.clear"),
61
+ run: clear,
62
+ }))
63
+
64
+ return () => {
65
+ clearInterval(timer)
66
+ void bridge()?.clear()?.catch(() => {})
67
+ }
68
+ },
69
+
70
+ main(ctx) {
71
+ const { app } = require("electron")
72
+ const paint = (n) => {
73
+ if (process.platform !== "darwin") return
74
+ try {
75
+ app.dock?.setBadge(n > 0 ? String(n) : "")
76
+ } catch {}
77
+ }
78
+ ctx.provide(Badge, {
79
+ set: (n) => paint(Number(n) || 0),
80
+ clear: () => paint(0),
81
+ })
82
+ ctx.log.write("info", "opencode-dock-badge main ready")
83
+ return () => paint(0)
84
+ },
85
+ })
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * opencode-dock-badge — server part.
3
+ *
4
+ * Deliberately minimal: the desktop GUI half already subscribes to the full event
5
+ * stream and owns the Dock badge. This export exists only so the package has a
6
+ * normal plugin id and can be listed / toggled like any other plugin.
7
+ */
8
+ export default {
9
+ id: "opencode-dock-badge",
10
+ setup: async () => {},
11
+ }