diff --git a/README.md b/README.md
index b820af4..2061a1b 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,10 @@ the toast. That's it.
something new arrives. Because why not.
- **`gitbell notify` from any script.** Agents, CI, cron: if it can run a
command, it can ring your bell. See below.
+- **Knows when an agent did it.** Commits and PRs from Claude Code, Copilot,
+ Codex, Cursor, OpenCode, Aider, Devin or Jules land in their own category,
+ the toast names the agent, and the click opens the agent's session when the
+ commit carried a link. Honest limits below.
- **Polite polling.** Honors GitHub's `X-Poll-Interval`, uses ETags so
unchanged polls cost zero rate limit, and backs off automatically when
throttled.
@@ -109,6 +113,32 @@ directory (`~/.local/share/dev.ejos.gitbell/` on Linux,
| `org:name` | every repository in an organization you belong to |
| `@me` | everything you watch or follow (your received-events feed) |
+## Agents 🤖
+
+GitHub has no "an agent did this" flag. GitBell reads the fingerprints the
+tools leave by default: `Co-Authored-By` trailers, session-link trailers
+(`Claude-Session:`, `Agent-Logs-Url:`), bot logins (`copilot-swe-agent[bot]`,
+`claude[bot]`), branch prefixes (`claude/`, `copilot/`, `cursor/`) and PR
+body markers. A hit moves the event to the **Agents** category with its own
+toggle and sound, the toast reads "🤖 Claude Code (@you) pushed to …", and
+the link prefers the agent's session. Your own agents count even with
+"ignore my own activity" on, because cloud sessions push as you.
+
+| Tool | Detected out of the box |
+|---|---|
+| Claude Code (CLI, desktop, web, GitHub Action) | yes |
+| GitHub Copilot coding agent | yes, always |
+| Codex (CLI, app, cloud) | yes |
+| Cursor agent, OpenCode, Aider, Devin, Jules | yes |
+| Gemini CLI, Cline, Roo Code, Kilo Code | no: they don't sign commits |
+
+**The catch:** detection only works when the tool signs. If you turn
+attribution off (every tool lets you), or use a tool that doesn't sign, the
+commit is indistinguishable from yours, for GitBell and for anyone. For
+those cases use the hook below: the agent tells GitBell directly, no git
+forensics involved. Models (DeepSeek, GPT, Claude) are not tools; the mark
+comes from whatever wraps them.
+
## Notify from scripts and agents 🤖
GitBell doubles as a local notification endpoint. While it's running in the
@@ -146,6 +176,27 @@ nothing was pushed yet. In `~/.claude/settings.json`:
}
```
+To be told about every commit Claude Code makes, whether or not it signs
+them, hook the `Bash` tool and look for `git commit`:
+
+```json
+{
+ "hooks": {
+ "PostToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "in=$(cat); echo \"$in\" | jq -e '.tool_input.command | test(\"git commit\")' >/dev/null && gitbell notify \"Claude Code committed\" --body \"$(echo \"$in\" | jq -r .cwd)\""
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
Any tool that can run a shell command works the same way: CI scripts, long
test runs, deploys, cron jobs.
diff --git a/index.html b/index.html
index 502a6c8..326ded0 100644
--- a/index.html
+++ b/index.html
@@ -225,6 +225,9 @@
+
diff --git a/src-tauri/src/notify.rs b/src-tauri/src/notify.rs
index 281cb56..65a7446 100644
--- a/src-tauri/src/notify.rs
+++ b/src-tauri/src/notify.rs
@@ -10,7 +10,10 @@ use tauri::AppHandle;
/// webview, and the webview renders content written by strangers on the
/// internet, so we don't trust it blindly (trust boundary, see SECURITY.md).
fn is_allowed_url(url: &str) -> bool {
- url.starts_with("https://github.com/")
+ // GitHub itself plus the places agent sessions live
+ ["https://github.com/", "https://claude.ai/", "https://chatgpt.com/", "https://cursor.com/"]
+ .iter()
+ .any(|p| url.starts_with(p))
}
#[tauri::command]
@@ -73,6 +76,7 @@ mod tests {
#[test]
fn only_github_https() {
assert!(is_allowed_url("https://github.com/tauri-apps/tauri/pull/1"));
+ assert!(is_allowed_url("https://claude.ai/code/session_01ABC"));
assert!(!is_allowed_url("http://github.com/x"));
assert!(!is_allowed_url("https://github.com.evil.io/x"));
assert!(!is_allowed_url("file:///etc/passwd"));
diff --git a/src/core/__tests__/agents.test.ts b/src/core/__tests__/agents.test.ts
new file mode 100644
index 0000000..b049dcd
--- /dev/null
+++ b/src/core/__tests__/agents.test.ts
@@ -0,0 +1,74 @@
+import { beforeAll, describe, expect, it } from "vitest";
+import { detectAgent } from "../agents";
+import { toNotice } from "../format";
+import { initLang } from "../i18n";
+import type { GhEvent } from "../types";
+
+const ev = (type: string, payload: Record, actor = "ejos"): GhEvent => ({
+ id: "9",
+ type,
+ actor: { login: actor, avatar_url: "" },
+ repo: { name: "acme/api" },
+ payload,
+ created_at: "2026-09-12T10:00:00Z",
+});
+const push = (message: string, author = { name: "Ana", email: "ana@example.com" }, ref = "refs/heads/main") =>
+ ev("PushEvent", { ref, size: 1, commits: [{ message, author }] });
+
+describe("detectAgent", () => {
+ it("claude code trailers, with session url when present", () => {
+ const h = detectAgent(push("fix: x\n\nCo-Authored-By: Claude \nClaude-Session: https://claude.ai/code/session_01A"));
+ expect(h?.agent).toBe("claude");
+ expect(h?.sessionUrl).toBe("https://claude.ai/code/session_01A");
+ expect(detectAgent(push("fix: x\n\nCo-Authored-By: Claude "))?.sessionUrl).toBeNull();
+ });
+
+ it("copilot: author name, committer bot login, Agent-Logs-Url", () => {
+ expect(detectAgent(push("x", { name: "Copilot", email: "1+copilot@users.noreply.github.com" }))?.agent).toBe("copilot");
+ expect(detectAgent(push("x\n\nAgent-Logs-Url: https://github.com/acme/api/sessions/abc"))?.sessionUrl)
+ .toBe("https://github.com/acme/api/sessions/abc");
+ expect(detectAgent(ev("PullRequestEvent", { pull_request: { head: { ref: "main" } } }, "copilot-swe-agent[bot]"))?.agent).toBe("copilot");
+ });
+
+ it("codex, cursor, opencode trailers", () => {
+ expect(detectAgent(push("x\n\nCo-authored-by: Codex "))?.agent).toBe("codex");
+ expect(detectAgent(push("x\n\nCo-authored-by: Cursor "))?.agent).toBe("cursor");
+ expect(detectAgent(push("x\n\n🤖 Generated with opencode\nCo-Authored-By: opencode "))?.agent).toBe("opencode");
+ });
+
+ it("branch prefixes and PR body markers", () => {
+ expect(detectAgent(ev("PullRequestEvent", { action: "opened", pull_request: { head: { ref: "claude/fix-login-abc" }, body: "" } }))?.agent).toBe("claude");
+ expect(detectAgent(ev("PullRequestEvent", { action: "opened", pull_request: { head: { ref: "feat/x" }, body: "…\n\n🤖 Generated with [Claude Code](https://claude.com)" } }))?.agent).toBe("claude");
+ expect(detectAgent(ev("CreateEvent", { ref_type: "branch", ref: "copilot/add-tests" }))?.agent).toBe("copilot");
+ });
+
+ it("humans stay humans", () => {
+ expect(detectAgent(push("feat: real work by a person"))).toBeNull();
+ expect(detectAgent(ev("PullRequestEvent", { pull_request: { head: { ref: "feature/login" }, body: "manual" } }))).toBeNull();
+ });
+
+ it("unknown [bot] actors are flagged as generic bots", () => {
+ expect(detectAgent(push("x"))).toBeNull();
+ expect(detectAgent(ev("WatchEvent", {}, "dependabot[bot]"))?.agent).toBe("bot");
+ });
+});
+
+describe("toNotice with agents", () => {
+ beforeAll(() => initLang("es", undefined));
+ const e = push("feat: x\n\nClaude-Session: https://claude.ai/code/session_01A");
+
+ it("moves the event to the agent category, names the agent, links the session", () => {
+ const n = toNotice(e)!;
+ expect(n.category).toBe("agent");
+ expect(n.agent).toBe("Claude Code");
+ expect(n.title).toBe("🤖 Claude Code (@ejos) hizo push a acme/api");
+ expect(n.url).toBe("https://claude.ai/code/session_01A");
+ });
+
+ it("falls back to the plain category when agents are disabled", () => {
+ const n = toNotice(e, false)!;
+ expect(n.category).toBe("push");
+ expect(n.agent).toBeUndefined();
+ expect(n.title).toBe("ejos hizo push a acme/api");
+ });
+});
diff --git a/src/core/agents.ts b/src/core/agents.ts
new file mode 100644
index 0000000..34201b0
--- /dev/null
+++ b/src/core/agents.ts
@@ -0,0 +1,165 @@
+// Who did this: a human or a coding agent? GitHub has no "agent" flag, so
+// we read the fingerprints each tool leaves behind. All of them are
+// optional and users can turn them off, so a hit is "probable", never
+// "certain". Sources for each signal are in APUNTES.md / README.
+
+import type { GhEvent } from "./types";
+
+export type AgentId =
+ | "claude" // Claude Code (CLI, web, GitHub Action)
+ | "copilot" // GitHub Copilot coding agent
+ | "codex" // OpenAI Codex
+ | "cursor" // Cursor agent
+ | "opencode" // OpenCode
+ | "devin" // Cognition Devin
+ | "jules" // Google Jules
+ | "aider" // Aider
+ | "bot"; // some other [bot] account
+
+export const AGENT_LABEL: Record = {
+ claude: "Claude Code",
+ copilot: "Copilot",
+ codex: "Codex",
+ cursor: "Cursor",
+ opencode: "OpenCode",
+ devin: "Devin",
+ jules: "Jules",
+ aider: "Aider",
+ bot: "Bot",
+};
+
+export interface AgentHit {
+ agent: AgentId;
+ /** link to the agent's own session/logs when the commit carried one */
+ sessionUrl: string | null;
+ /** which fingerprint fired, for debugging and tests */
+ signal: string;
+}
+
+// ---- fingerprints -----------------------------------------------------------
+
+/** GitHub logins that are agents. Matched case-insensitively, exact. */
+const LOGINS: [string, AgentId][] = [
+ ["claude[bot]", "claude"],
+ ["copilot-swe-agent[bot]", "copilot"],
+ ["copilot", "copilot"],
+ ["chatgpt-codex-connector[bot]", "codex"],
+ ["cursor[bot]", "cursor"],
+ ["devin-ai-integration[bot]", "devin"],
+ ["google-labs-jules[bot]", "jules"],
+];
+
+/** Email domains/addresses that show up in Co-Authored-By trailers or as commit authors. */
+const EMAILS: [RegExp, AgentId][] = [
+ [/@anthropic\.com$/i, "claude"],
+ [/^noreply@openai\.com$/i, "codex"],
+ [/@cursor\.com$/i, "cursor"],
+ [/@opencode\.ai$/i, "opencode"],
+];
+
+/** Branch prefixes the cloud agents use for their work. */
+const BRANCHES: [RegExp, AgentId][] = [
+ [/^claude\//, "claude"],
+ [/^copilot\//, "copilot"],
+ [/^cursor\//, "cursor"],
+ [/^codex\//, "codex"],
+ [/^devin\//, "devin"],
+];
+
+/** Free-text markers in commit messages or PR bodies. */
+const TEXT: [RegExp, AgentId][] = [
+ [/Generated with \[?Claude Code\]?/i, "claude"],
+ [/^Claude-Session:\s*https?:\/\/\S+/im, "claude"],
+ [/^Agent-Logs-Url:\s*https?:\/\/\S+/im, "copilot"],
+ [/Generated with opencode/i, "opencode"],
+ [/^Co-Authored-By:.*\bCodex\b/im, "codex"],
+ [/^Co-Authored-By:.*\bCursor\b/im, "cursor"],
+ [/^Co-Authored-By:.*\bClaude\b/im, "claude"],
+ [/\(aider\)/i, "aider"],
+];
+
+const SESSION_TRAILERS = [
+ /^Claude-Session:\s*(https?:\/\/\S+)/im,
+ /^Agent-Logs-Url:\s*(https?:\/\/\S+)/im,
+];
+
+// ---- detection --------------------------------------------------------------
+
+function fromLogin(login: string | undefined): AgentHit | null {
+ if (!login) return null;
+ const l = login.toLowerCase();
+ for (const [name, agent] of LOGINS) if (l === name) return { agent, sessionUrl: null, signal: `login:${login}` };
+ if (l.endsWith("[bot]")) return { agent: "bot", sessionUrl: null, signal: `login:${login}` };
+ return null;
+}
+
+function fromText(text: string | undefined, where: string): AgentHit | null {
+ if (!text) return null;
+ for (const [re, agent] of TEXT) {
+ if (re.test(text)) {
+ let sessionUrl: string | null = null;
+ for (const tr of SESSION_TRAILERS) {
+ const m = tr.exec(text);
+ if (m) {
+ sessionUrl = m[1];
+ break;
+ }
+ }
+ return { agent, sessionUrl, signal: `${where}:${re.source.slice(0, 24)}` };
+ }
+ }
+ return null;
+}
+
+function fromEmail(email: string | undefined, name: string | undefined, where: string): AgentHit | null {
+ if (email) for (const [re, agent] of EMAILS) if (re.test(email)) return { agent, sessionUrl: null, signal: `${where}:${email}` };
+ if (name && /^copilot$/i.test(name)) return { agent: "copilot", sessionUrl: null, signal: `${where}:${name}` };
+ if (name && /\(aider\)/i.test(name)) return { agent: "aider", sessionUrl: null, signal: `${where}:${name}` };
+ return null;
+}
+
+function fromBranch(ref: string | undefined): AgentHit | null {
+ if (!ref) return null;
+ for (const [re, agent] of BRANCHES) if (re.test(ref)) return { agent, sessionUrl: null, signal: `branch:${ref}` };
+ return null;
+}
+
+/**
+ * Inspect an event for agent fingerprints. Order: actor login (strongest),
+ * then per-type payload details (commit trailers/authors, PR branch/body).
+ */
+export function detectAgent(ev: GhEvent): AgentHit | null {
+ const p = ev.payload;
+ const byActor = fromLogin(ev.actor?.login);
+ if (byActor && byActor.agent !== "bot") return byActor;
+
+ let hit: AgentHit | null = null;
+ switch (ev.type) {
+ case "PushEvent": {
+ const commits: { message?: string; author?: { name?: string; email?: string } }[] = p.commits ?? [];
+ for (const c of commits) {
+ hit = fromText(c.message, "commit") ?? fromEmail(c.author?.email, c.author?.name, "author");
+ if (hit) break;
+ }
+ hit ??= fromBranch((p.ref as string | undefined)?.replace(/^refs\/heads\//, ""));
+ break;
+ }
+ case "PullRequestEvent":
+ case "PullRequestReviewEvent":
+ case "PullRequestReviewCommentEvent": {
+ const pr = p.pull_request ?? {};
+ hit = fromLogin(pr.user?.login)
+ ?? fromBranch(pr.head?.ref)
+ ?? fromText(pr.body, "pr-body")
+ ?? fromText(pr.title, "pr-title");
+ if (hit?.agent === "bot" && !byActor) hit = null; // PR by a generic bot but human actor: not ours to flag
+ break;
+ }
+ case "CreateEvent":
+ hit = fromBranch(p.ref);
+ break;
+ default:
+ break;
+ }
+ return hit ?? byActor;
+}
diff --git a/src/core/format.ts b/src/core/format.ts
index b777d72..9e3e35c 100644
--- a/src/core/format.ts
+++ b/src/core/format.ts
@@ -1,6 +1,7 @@
// GitHub event -> human sentence. This is the file you'll edit most when
// you want the toasts to say something different.
+import { AGENT_LABEL, detectAgent } from "./agents";
import { t } from "./i18n";
import type { EventCategory, GhEvent, Notice } from "./types";
@@ -28,12 +29,19 @@ const shortRef = (ref: string | undefined) => (ref ?? "").replace(/^refs\/heads\
const firstLine = (s: string | undefined) => (s ?? "").split("\n")[0].trim();
const clip = (s: string, n = 90) => (s.length > n ? s.slice(0, n - 1) + "…" : s);
-/** Returns null for event types we don't narrate. */
-export function toNotice(ev: GhEvent): Notice | null {
- const category = categoryOf(ev.type);
+/**
+ * Returns null for event types we don't narrate. When a coding agent did it
+ * and `agentsEnabled`, the notice moves to the "agent" category, the agent
+ * is named as the actor, and the link prefers the agent's session.
+ */
+export function toNotice(ev: GhEvent, agentsEnabled = true): Notice | null {
+ let category = categoryOf(ev.type);
if (!category) return null;
- const who = ev.actor.login;
+ const hit = agentsEnabled ? detectAgent(ev) : null;
+ const agentLabel = hit ? AGENT_LABEL[hit.agent] : undefined;
+ if (hit) category = "agent";
+ const who = agentLabel ? `${agentLabel} (@${ev.actor.login})` : ev.actor.login;
const repo = ev.repo.name;
const p = ev.payload;
const repoUrl = `https://github.com/${repo}`;
@@ -142,9 +150,15 @@ export function toNotice(ev: GhEvent): Notice | null {
}
}
+ if (hit) {
+ title = `🤖 ${title}`;
+ if (hit.sessionUrl) url = hit.sessionUrl;
+ }
+
return {
id: ev.id,
category,
+ agent: agentLabel,
title,
body,
url,
diff --git a/src/core/i18n.ts b/src/core/i18n.ts
index e190e4d..9b51327 100644
--- a/src/core/i18n.ts
+++ b/src/core/i18n.ts
@@ -43,6 +43,9 @@ const dict = {
"cat.release": "Releases",
"cat.social": "Stars, forks, miembros",
"cat.external": "Avisos externos (gitbell notify)",
+ "cat.agent": "Agentes (Claude, Copilot, Codex…)",
+ "options.agentsOwn": "Avisar aunque el agente actúe con mi cuenta",
+ "recent.agent": "Agente",
// sounds
"sounds.title": "Sonidos",
"sounds.enabled": "Reproducir sonido con cada aviso",
@@ -187,6 +190,9 @@ const dict = {
"cat.release": "Releases",
"cat.social": "Stars, forks, members",
"cat.external": "External notices (gitbell notify)",
+ "cat.agent": "Agents (Claude, Copilot, Codex…)",
+ "options.agentsOwn": "Notify even when the agent acts under my account",
+ "recent.agent": "Agent",
"sounds.title": "Sounds",
"sounds.enabled": "Play a sound with every notification",
"sounds.volume": "Volume",
diff --git a/src/core/poller.ts b/src/core/poller.ts
index 64a8033..5855780 100644
--- a/src/core/poller.ts
+++ b/src/core/poller.ts
@@ -153,13 +153,17 @@ export class Poller {
if (!last) return []; // first contact: just remember, don't spam
- const { events: wanted, ignoreOwn, login } = this.settings;
+ const { events: wanted, ignoreOwn, agentsOwn, login } = this.settings;
const out: Notice[] = [];
for (const ev of events) {
if (BigInt(ev.id) <= BigInt(last)) break;
- if (ignoreOwn && login && ev.actor.login === login) continue;
- const n = toNotice(ev);
+ const n = toNotice(ev, wanted.agent);
if (!n || !wanted[n.category]) continue;
+ // your own activity is noise... unless an agent did it under your name:
+ // cloud sessions push with your credentials and that's exactly the
+ // "did my agent finish?" moment
+ const mine = !!login && ev.actor.login === login;
+ if (mine && ignoreOwn && !(n.agent && agentsOwn)) continue;
out.push(n);
}
return out;
diff --git a/src/core/types.ts b/src/core/types.ts
index c4077cc..83ae57d 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -13,7 +13,8 @@ export type EventCategory =
| "branch"
| "release"
| "social"
- | "external";
+ | "external"
+ | "agent";
export const ALL_CATEGORIES: EventCategory[] = [
"push",
@@ -25,6 +26,7 @@ export const ALL_CATEGORIES: EventCategory[] = [
"release",
"social",
"external",
+ "agent",
];
export function categoryLabel(cat: EventCategory): string {
@@ -48,6 +50,8 @@ export interface Settings {
pollSeconds: number;
/** skip events where actor === login (you already know what you did) */
ignoreOwn: boolean;
+ /** ...except when an agent did it under your account (cloud sessions push as you) */
+ agentsOwn: boolean;
/** master switch for audio */
soundsEnabled: boolean;
/** 0..1 */
@@ -75,9 +79,11 @@ export const DEFAULT_SETTINGS: Settings = {
release: true,
social: false,
external: true,
+ agent: true,
},
pollSeconds: 60,
ignoreOwn: true,
+ agentsOwn: true,
soundsEnabled: true,
volume: 0.8,
glass: 0.7,
@@ -91,6 +97,7 @@ export const DEFAULT_SETTINGS: Settings = {
release: "sound2",
social: "none",
external: "sound1",
+ agent: "sound2",
},
};
@@ -109,6 +116,8 @@ export interface GhEvent {
export interface Notice {
id: string;
category: EventCategory;
+ /** set when a coding agent did it; label like "Claude Code" */
+ agent?: string;
title: string;
body: string;
url: string;
diff --git a/src/ui/preview.ts b/src/ui/preview.ts
index 68a635a..a7dc619 100644
--- a/src/ui/preview.ts
+++ b/src/ui/preview.ts
@@ -34,6 +34,7 @@ export function preview(): void {
let token: string | null = noToken ? null : "preview";
const recent = noToken ? [] : [
+ { id: "4", category: "agent" as const, agent: "Claude Code", title: "🤖 Claude Code (@eJosR-Coding) abrió PR #14 en eJosR-Coding/gitbell", body: "feat(agents): detect coding agents", url: "https://claude.ai/code/session_01X", actor: "eJosR-Coding", avatar: "https://avatars.githubusercontent.com/u/9919?v=4", repo: "eJosR-Coding/gitbell", at: new Date(Date.now() - 3 * 60e3).toISOString() },
{ id: "3", category: "pr" as const, title: "ana abrió PR #12 en eJosR-Coding/gitbell", body: "feat(home): activity first, settings second", url: "https://github.com/eJosR-Coding/gitbell/pull/12", actor: "ana", avatar: "https://avatars.githubusercontent.com/u/583231?v=4", repo: "eJosR-Coding/gitbell", at: new Date(Date.now() - 12 * 60e3).toISOString() },
{ id: "2", category: "external" as const, title: "Claude Code terminó", body: "/home/ejos/Documents/proyectos_code/gitbell", url: "", actor: "cli", avatar: "", repo: "", at: new Date(Date.now() - 55 * 60e3).toISOString() },
{ id: "1", category: "push" as const, title: "ana hizo push a eJosR-Coding/gitbell", body: "2 commits en main: feat(i18n): spanish and english UI", url: "https://github.com/eJosR-Coding/gitbell", actor: "ana", avatar: "https://avatars.githubusercontent.com/u/583231?v=4", repo: "eJosR-Coding/gitbell", at: new Date(Date.now() - 7200e3).toISOString() },
diff --git a/src/ui/styles.css b/src/ui/styles.css
index 3c96657..444df73 100644
--- a/src/ui/styles.css
+++ b/src/ui/styles.css
@@ -312,3 +312,11 @@ h3.sub { font-size: 12px; font-weight: 600; color: var(--muted); margin: 10px 0
background: color-mix(in srgb, var(--card) calc(var(--glass, 0.7) * 100%), transparent);
border-color: var(--glass-line);
}
+
+/* ===== agent tag in the activity list */
+.tag {
+ display: inline-block; vertical-align: 1px;
+ font-size: 10.5px; font-weight: 600; letter-spacing: 0.02em;
+ color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent);
+ border-radius: 999px; padding: 1px 7px;
+}
diff --git a/src/ui/ui.ts b/src/ui/ui.ts
index ec5be96..33bfa05 100644
--- a/src/ui/ui.ts
+++ b/src/ui/ui.ts
@@ -373,6 +373,10 @@ export class Ui {
void this.commit({ ...this.settings, pollSeconds: v });
});
+ const agentsOwn = $("#agents-own");
+ agentsOwn.checked = this.settings.agentsOwn;
+ agentsOwn.addEventListener("change", () => void this.commit({ ...this.settings, agentsOwn: agentsOwn.checked }));
+
const ignoreOwn = $("#ignore-own");
ignoreOwn.checked = this.settings.ignoreOwn;
ignoreOwn.addEventListener("change", () => void this.commit({ ...this.settings, ignoreOwn: ignoreOwn.checked }));
@@ -493,6 +497,12 @@ export class Ui {
else img.replaceWith(Object.assign(document.createElement("span"), { className: "notice-dot" }));
const a = li.querySelector(".notice-title")!;
a.textContent = n.title;
+ if (n.agent) {
+ const tag = document.createElement("span");
+ tag.className = "tag";
+ tag.textContent = n.agent;
+ a.after(" ", tag);
+ }
if (n.url) {
a.addEventListener("click", (e) => {
e.preventDefault();