letagents 0.12.33 → 0.12.35

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.
Files changed (67) hide show
  1. package/README.md +0 -2
  2. package/dist/mcp/codex-session/app-server.js +16 -2
  3. package/dist/mcp/codex-session/session-start.js +1 -1
  4. package/dist/mcp/server/runtime/messages.js +18 -1
  5. package/dist/mcp/server/tools/tasks/verdict-tools.js +1 -1
  6. package/dist/mcp/server/tools/wake-rules.js +2 -1
  7. package/dist/shared/presence-change-channel.js +2 -0
  8. package/package.json +2 -2
  9. package/shared/agent-branch.d.mts +5 -0
  10. package/shared/agent-branch.mjs +28 -0
  11. package/shared/code-block.css +193 -0
  12. package/shared/code-highlighting.d.mts +109 -0
  13. package/shared/code-highlighting.mjs +345 -0
  14. package/shared/codex-owner-isolation.d.mts +34 -0
  15. package/shared/codex-owner-isolation.mjs +210 -0
  16. package/shared/message-link-preview-store.d.mts +10 -0
  17. package/shared/message-link-preview-store.mjs +77 -0
  18. package/shared/message-link-previews.d.mts +16 -0
  19. package/shared/message-link-previews.mjs +70 -0
  20. package/shared/message-pin-store.d.mts +20 -0
  21. package/shared/message-pin-store.mjs +102 -0
  22. package/shared/message-pins.d.mts +25 -0
  23. package/shared/message-pins.mjs +16 -0
  24. package/shared/message-reaction-store.d.mts +51 -0
  25. package/shared/message-reaction-store.mjs +290 -0
  26. package/shared/message-reactions.d.mts +73 -0
  27. package/shared/message-reactions.mjs +130 -0
  28. package/shared/message-search-controller.d.mts +38 -0
  29. package/shared/message-search-controller.mjs +99 -0
  30. package/shared/message-search.d.mts +46 -0
  31. package/shared/message-search.mjs +114 -0
  32. package/shared/paced-queue.d.mts +28 -0
  33. package/shared/paced-queue.mjs +159 -0
  34. package/shared/permission-review.d.mts +19 -0
  35. package/shared/permission-review.mjs +238 -15
  36. package/shared/room-message-urls.d.mts +25 -0
  37. package/shared/room-message-urls.mjs +119 -0
  38. package/shared/room-notification-client.d.mts +18 -0
  39. package/shared/room-notification-client.mjs +140 -0
  40. package/shared/room-notification-preferences.d.mts +17 -0
  41. package/shared/room-notification-preferences.mjs +34 -0
  42. package/shared/room-resource-invalidation.d.mts +5 -0
  43. package/shared/room-resource-invalidation.mjs +6 -0
  44. package/shared/room-turn-no-reply.d.mts +1 -0
  45. package/shared/room-turn-no-reply.mjs +1 -0
  46. package/shared/room-typing.d.mts +16 -0
  47. package/shared/room-typing.mjs +112 -0
  48. package/shared/room-unread-client.ts +128 -0
  49. package/shared/room-unread.d.mts +25 -0
  50. package/shared/room-unread.mjs +57 -0
  51. package/shared/room-unread.test.mjs +91 -0
  52. package/shared/rooms/RoomsDirectory.vue +1 -1
  53. package/shared/rooms/directory.ts +1 -0
  54. package/shared/slash-commands.d.mts +31 -0
  55. package/shared/slash-commands.mjs +48 -0
  56. package/shared/slash-commands.test.mjs +26 -0
  57. package/shared/ui/MessageReactionBar.vue +65 -0
  58. package/shared/ui/MessageReactionPicker.vue +291 -0
  59. package/shared/ui/MessageSearchResults.vue +106 -0
  60. package/shared/ui/PinnedMessages.vue +124 -0
  61. package/shared/ui/TypingIndicator.vue +22 -0
  62. package/shared/ui/WakeRulesPanel.vue +1 -0
  63. package/shared/ui/message-reactions.css +254 -0
  64. package/shared/ui/message-search.css +186 -0
  65. package/shared/ui/useComposerSlashCommands.ts +92 -0
  66. package/shared/wake-rules.d.mts +8 -3
  67. package/shared/wake-rules.mjs +20 -4
package/README.md CHANGED
@@ -212,8 +212,6 @@ The API runs at `http://localhost:3001`. Point `LETAGENTS_API_URL` at your serve
212
212
 
213
213
  Optional — **long room long-polls** (multi-hour `wait_for_messages` / `GET …/messages/poll`): set the **same** `LETAGENTS_POLL_MAX_MS` on **both** the API process and any MCP client you run from source (milliseconds; default `180000`).
214
214
 
215
- Optional — **offline-with-work notice grace**: set `LETAGENTS_LIVENESS_NOTICE_AFTER_MS` on the API process (milliseconds; default `1800000`, or 30 minutes). The room is told about an unreachable worker only while it holds an active work lease, and only after this grace; ordinary connects and disconnects are never posted (the roster shows presence). Internal transport staleness remains 2 minutes for routing and diagnostics.
216
-
217
215
  The API now uses PostgreSQL with Drizzle ORM. `DB_URL` must be set before starting the server or running migrations.
218
216
 
219
217
  Useful database commands:
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "child_process";
2
2
  import { createServer } from "net";
3
+ import { codexOwnerIsolationOverrides } from "../../../shared/codex-owner-isolation.mjs";
3
4
  const DEFAULT_SERVER_HOST = "127.0.0.1";
4
5
  const DEFAULT_TIMEOUT_MS = 15_000;
5
6
  function readyUrlFromServerUrl(serverUrl) {
@@ -62,8 +63,21 @@ export async function resolveCodexServerUrl(explicitServerUrl) {
62
63
  }
63
64
  return allocateLoopbackServerUrl();
64
65
  }
65
- export function launchAppServer(serverUrl, codexBin) {
66
- const child = spawn(codexBin, ["app-server", "--listen", serverUrl], {
66
+ export function codexAppServerArgs(serverUrl, configOverrides) {
67
+ return ["app-server", ...configOverrides.flatMap((override) => ["-c", override]), "--listen", serverUrl];
68
+ }
69
+ /**
70
+ * The session works for the room, so its app-server gets none of the owner's
71
+ * Codex plugins, connectors, hooks, personal skills or MCP servers. It keeps
72
+ * the owner's LetAgents server, which it joins the room with. A launch that
73
+ * cannot list the owner's servers, or whose project changes the LetAgents
74
+ * server, fails rather than starting.
75
+ */
76
+ export async function launchAppServer(serverUrl, codexBin, options = {}) {
77
+ const overrides = await codexOwnerIsolationOverrides(codexBin, { cwd: options.cwd, env: process.env });
78
+ const child = spawn(codexBin, codexAppServerArgs(serverUrl, overrides), {
79
+ // Start where the servers were listed, so the project config matches.
80
+ ...(options.cwd ? { cwd: options.cwd } : {}),
67
81
  detached: true,
68
82
  stdio: "ignore",
69
83
  });
@@ -90,7 +90,7 @@ export async function startLocalCodexSession(input) {
90
90
  let startupSucceeded = false;
91
91
  try {
92
92
  if (launchedServer) {
93
- serverPid = launchAppServer(serverUrl, codexBin);
93
+ serverPid = await launchAppServer(serverUrl, codexBin, { cwd });
94
94
  if (serverPid) {
95
95
  registerLaunchedAppServer(serverPid);
96
96
  }
@@ -122,11 +122,28 @@ function withThreadMetadata(record, recordsById, summaries) {
122
122
  },
123
123
  };
124
124
  }
125
+ /**
126
+ * Who reacted is worth an agent's attention; an empty list on every message,
127
+ * or an avatar address per reactor, is not.
128
+ */
129
+ function withAgentReadableReactions(record) {
130
+ const { reactions, ...rest } = record;
131
+ if (!Array.isArray(reactions))
132
+ return record;
133
+ const readable = reactions.filter(isRecord).map((reaction) => ({
134
+ emoji: reaction.emoji,
135
+ count: reaction.count,
136
+ by: (Array.isArray(reaction.reactors) ? reaction.reactors : [])
137
+ .filter(isRecord)
138
+ .map((reactor) => reactor.name ?? reactor.login),
139
+ }));
140
+ return readable.length ? { ...rest, reactions: readable } : rest;
141
+ }
125
142
  function toAgentReadableMessage(message, recordsById, summaries) {
126
143
  if (!message || typeof message !== "object") {
127
144
  return message;
128
145
  }
129
- const record = withThreadMetadata(message, recordsById, summaries);
146
+ const record = withThreadMetadata(withAgentReadableReactions(message), recordsById, summaries);
130
147
  const kind = normalizeAgentPromptKind(record.agent_prompt_kind);
131
148
  const text = typeof record.text === "string" ? record.text : null;
132
149
  if (!kind || text === null) {
@@ -6,7 +6,7 @@ import { jsonToolResponse, taskToolError } from "./response.js";
6
6
  import { taskReviewIdentitySchema } from "./schemas.js";
7
7
  export function registerTaskVerdictTools(server) {
8
8
  server.tool("submit_review_verdict", "Submit a GitHub review verdict through the durable effect journal. Requires this exact worker session to hold an active review lease. Success means GitHub confirmed publication; pending, failed, or uncertain effects keep their original idempotency key. Ambiguous outcomes are reconciled by correlation lookup and are never blindly retried.", {
9
- task_id: z.string().describe("Task in review, e.g. 'task_1'."),
9
+ task_id: z.string().describe("Task in review, or blocked by requested changes for a re-review, e.g. 'task_1'."),
10
10
  verdict: z.enum(["approve", "request_changes", "comment"]).describe("GitHub review verdict."),
11
11
  body: z.string().max(65_536).optional().describe("Review explanation. Empty or junk blocking verdicts are quarantined."),
12
12
  expected_head_sha: z.string().regex(/^[0-9a-fA-F]{40}$/).describe("Exact 40-hex pull request head SHA that was reviewed."),
@@ -30,6 +30,7 @@ export function registerWakeRuleTools(server) {
30
30
  + "github.review_submitted {pr | mine: true, states?: [approved, changes_requested, commented]}; "
31
31
  + "github.pr_closed {pr | mine: true, merged_only?}. "
32
32
  + "`mine` means the branches and pull requests of tasks you are working on. Rules expire after 24 hours unless you set expires_at (at most 7 days). "
33
+ + "A rule on one pull request ends when it merges (or stays closed for 10 minutes), and a task rule ends when the task is done or cancelled; you are woken once to say your wait ended. "
33
34
  + "Adding the same rule twice returns the existing one. People see what you are waiting for and can cancel it.", {
34
35
  event: z.enum(WAKE_RULE_EVENTS),
35
36
  arguments: z.record(z.string(), z.unknown()).optional().describe("What exactly to wait for; see the event list above."),
@@ -58,7 +59,7 @@ export function registerWakeRuleTools(server) {
58
59
  return toolError(error);
59
60
  }
60
61
  });
61
- server.tool("list_wake_rules", "List what agents in this room are waiting for, and wake rules that recently fired, expired or were cancelled. Check this on resume to see what you were waiting for.", { ...scope, agent_key: z.string().optional().describe("Only this agent’s rules.") }, async ({ room_id, agent_key }) => {
62
+ server.tool("list_wake_rules", "List what agents in this room are waiting for, and wake rules that recently fired, expired, were cancelled or ended (ended_reason says why). Check this on resume to see what you were waiting for.", { ...scope, agent_key: z.string().optional().describe("Only this agent’s rules.") }, async ({ room_id, agent_key }) => {
62
63
  try {
63
64
  const { target, local } = await roomTarget(room_id);
64
65
  if (local)
@@ -0,0 +1,2 @@
1
+ /** Postgres NOTIFY channel written by the room_agent_presence_notify trigger (migration 0105). */
2
+ export const PRESENCE_CHANGE_CHANNEL = "letagents_presence_changes";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.12.33",
3
+ "version": "0.12.35",
4
4
  "description": "Let Agents Chat \u2014 MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",
@@ -70,7 +70,7 @@
70
70
  "esbuild": "^0.25.12"
71
71
  },
72
72
  "fast-uri": "3.1.8",
73
- "hono": "4.13.5",
73
+ "hono": "4.13.8",
74
74
  "ip-address": "10.7.2"
75
75
  }
76
76
  }
@@ -0,0 +1,5 @@
1
+ /** The part of a leased branch's name that names the agent. */
2
+ export function agentBranchSegment(agentKey: string): string;
3
+ export function leasedBranchRef(taskId: string, agentKey: string): string;
4
+ /** True when `branch` is a branch LetAgents would lease to this agent, for any task. */
5
+ export function isAgentBranch(branch: unknown, agentKey: unknown): boolean;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The branches LetAgents leases to an agent: `letagents/<task>/<agent>`. The
3
+ * server names a task's branch with this module, and the desktop uses it to
4
+ * tell an agent's own branches from every other branch.
5
+ */
6
+
7
+ /** The part of a leased branch's name that names the agent. */
8
+ export function agentBranchSegment(agentKey) {
9
+ const slug = agentKey
10
+ .trim()
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9._-]+/g, "-")
13
+ .replace(/^-+|-+$/g, "")
14
+ .slice(0, 64);
15
+ return slug || "agent";
16
+ }
17
+
18
+ export function leasedBranchRef(taskId, agentKey) {
19
+ return `letagents/${taskId}/${agentBranchSegment(agentKey)}`;
20
+ }
21
+
22
+ /** True when `branch` is a branch LetAgents would lease to this agent, for any task. */
23
+ export function isAgentBranch(branch, agentKey) {
24
+ if (typeof branch !== "string" || typeof agentKey !== "string" || !agentKey.trim()) return false;
25
+ const [prefix, task, agent, ...rest] = branch.split("/");
26
+ return prefix === "letagents" && rest.length === 0 && typeof task === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(task)
27
+ && agent === agentBranchSegment(agentKey);
28
+ }
@@ -0,0 +1,193 @@
1
+ /* ─── Shared Fenced Code Block & Syntax Highlighting Styles ─── */
2
+
3
+ .fenced-code-block {
4
+ position: relative;
5
+ max-width: 100%;
6
+ margin: 0.75em 0;
7
+ padding: 0;
8
+ overflow: hidden;
9
+ border: 1px solid var(--border);
10
+ border-radius: var(--radius-md, 8px);
11
+ background: var(--code-bg, color-mix(in srgb, var(--bg) 88%, transparent));
12
+ color: var(--code-text, var(--text));
13
+ font-family: var(--font-mono);
14
+ font-size: 0.82rem;
15
+ line-height: 1.55;
16
+ }
17
+
18
+ .fenced-code-block-header {
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: space-between;
22
+ padding: 6px 12px;
23
+ background: var(--code-header-bg, color-mix(in srgb, var(--text) 4%, transparent));
24
+ border-bottom: 1px solid var(--border);
25
+ user-select: none;
26
+ font-family: var(--font-sans);
27
+ font-size: 0.75rem;
28
+ line-height: 1.2;
29
+ }
30
+
31
+ .fenced-code-block-lang {
32
+ color: var(--code-lang, var(--text-secondary));
33
+ font-weight: 500;
34
+ text-transform: lowercase;
35
+ }
36
+
37
+ .fenced-code-block-lang[data-lang]::before {
38
+ content: attr(data-lang);
39
+ }
40
+
41
+ .fenced-code-block-copy {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ margin-left: auto;
46
+ padding: 3px 8px;
47
+ border: 1px solid var(--border);
48
+ border-radius: var(--radius-sm, 4px);
49
+ background: var(--code-copy-bg, transparent);
50
+ color: var(--code-copy-text, var(--text-secondary));
51
+ font-family: var(--font-sans);
52
+ font-size: 0.72rem;
53
+ font-weight: 500;
54
+ line-height: 1.2;
55
+ cursor: pointer;
56
+ transition: color var(--duration-fast, 150ms) var(--ease-out, ease),
57
+ background var(--duration-fast, 150ms) var(--ease-out, ease),
58
+ border-color var(--duration-fast, 150ms) var(--ease-out, ease);
59
+ }
60
+
61
+ @media (hover: hover) and (pointer: fine) {
62
+ .fenced-code-block-copy:hover {
63
+ background: var(--accent-hover, rgba(255, 255, 255, 0.08));
64
+ color: var(--text);
65
+ border-color: var(--border-strong);
66
+ }
67
+ }
68
+
69
+ .fenced-code-block-copy:focus-visible {
70
+ outline: 2px solid var(--blue, #3b82f6);
71
+ outline-offset: 1px;
72
+ }
73
+
74
+ .fenced-code-block-copy::before {
75
+ content: "Copy";
76
+ }
77
+
78
+ .fenced-code-block-copy[data-copied="true"] {
79
+ color: var(--green-text, #4ade80);
80
+ border-color: color-mix(in srgb, var(--green-text, #4ade80) 40%, transparent);
81
+ }
82
+
83
+ .fenced-code-block-copy[data-copied="true"]::before {
84
+ content: "Copied";
85
+ }
86
+
87
+ .fenced-code-block-copy[data-copied="error"] {
88
+ color: var(--red-text, #f87171);
89
+ border-color: color-mix(in srgb, var(--red-text, #f87171) 40%, transparent);
90
+ }
91
+
92
+ .fenced-code-block-copy[data-copied="error"]::before {
93
+ content: "Failed";
94
+ }
95
+
96
+ /*
97
+ * Eleven different surfaces across desktop and web (e.g. chat messages, long message reader,
98
+ * board task inspector, thread panel, events view, markdown editor preview, agent inspector)
99
+ * define conflicting styles for `pre` with varying selector specificity.
100
+ * The `!important` declarations below ensure the fenced code block pre maintains a clean,
101
+ * borderless, transparent container with standard monospace sizing across all surfaces.
102
+ */
103
+ .fenced-code-block pre {
104
+ margin: 0 !important;
105
+ padding: 10px 14px !important;
106
+ overflow-x: auto !important;
107
+ border: none !important;
108
+ border-radius: 0 !important;
109
+ background: transparent !important;
110
+ line-height: 1.55;
111
+ font-family: var(--font-mono);
112
+ font-size: 0.82rem;
113
+ white-space: pre;
114
+ }
115
+
116
+ .fenced-code-block code.fenced-code-block-content {
117
+ display: block;
118
+ padding: 0;
119
+ overflow-x: visible;
120
+ background: transparent;
121
+ color: inherit;
122
+ font-family: inherit;
123
+ font-size: inherit;
124
+ line-height: inherit;
125
+ white-space: pre;
126
+ }
127
+
128
+ /* ─── Syntax Highlighting Token Mappings ─── */
129
+ .hljs-comment,
130
+ .hljs-quote {
131
+ color: var(--code-comment, var(--text-secondary));
132
+ font-style: italic;
133
+ }
134
+
135
+ .hljs-keyword,
136
+ .hljs-selector-tag,
137
+ .hljs-subst {
138
+ color: var(--code-keyword, var(--task-assigned));
139
+ }
140
+
141
+ .hljs-string,
142
+ .hljs-doctag,
143
+ .hljs-regexp {
144
+ color: var(--code-string, var(--green-text));
145
+ }
146
+
147
+ .hljs-number,
148
+ .hljs-literal {
149
+ color: var(--code-number, var(--amber-text));
150
+ }
151
+
152
+ .hljs-title,
153
+ .hljs-title.function_,
154
+ .hljs-title.class_,
155
+ .hljs-section {
156
+ color: var(--code-function, var(--blue-text));
157
+ }
158
+
159
+ .hljs-variable,
160
+ .hljs-template-variable,
161
+ .hljs-attr,
162
+ .hljs-attribute {
163
+ color: var(--code-variable, var(--task-in-review));
164
+ }
165
+
166
+ .hljs-type,
167
+ .hljs-built_in,
168
+ .hljs-name {
169
+ color: var(--code-type, var(--red-text));
170
+ }
171
+
172
+ .hljs-meta,
173
+ .hljs-tag {
174
+ color: var(--code-comment, var(--text-secondary));
175
+ }
176
+
177
+ .hljs-addition {
178
+ color: var(--code-string, var(--green-text));
179
+ background-color: var(--green-dim, rgba(34, 197, 94, 0.12));
180
+ }
181
+
182
+ .hljs-deletion {
183
+ color: var(--code-type, var(--red-text));
184
+ background-color: var(--red-dim, rgba(239, 68, 68, 0.12));
185
+ }
186
+
187
+ .hljs-emphasis {
188
+ font-style: italic;
189
+ }
190
+
191
+ .hljs-strong {
192
+ font-weight: bold;
193
+ }
@@ -0,0 +1,109 @@
1
+ export type CanonicalLanguage =
2
+ | 'javascript'
3
+ | 'typescript'
4
+ | 'python'
5
+ | 'bash'
6
+ | 'json'
7
+ | 'yaml'
8
+ | 'xml'
9
+ | 'css'
10
+ | 'sql'
11
+ | 'diff'
12
+ | 'rust'
13
+ | 'go'
14
+ | 'c'
15
+ | 'cpp'
16
+ | 'java'
17
+ | 'csharp'
18
+ | 'dockerfile'
19
+ | 'ruby'
20
+ | 'php'
21
+ | 'swift'
22
+ | 'kotlin'
23
+ | 'ini';
24
+
25
+ export type LanguageAlias =
26
+ | 'js'
27
+ | 'jsx'
28
+ | 'mjs'
29
+ | 'cjs'
30
+ | 'ts'
31
+ | 'tsx'
32
+ | 'mts'
33
+ | 'cts'
34
+ | 'py'
35
+ | 'sh'
36
+ | 'zsh'
37
+ | 'shell'
38
+ | 'yml'
39
+ | 'html'
40
+ | 'xhtml'
41
+ | 'svg'
42
+ | 'rs'
43
+ | 'golang'
44
+ | 'c++'
45
+ | 'cc'
46
+ | 'hpp'
47
+ | 'h'
48
+ | 'cs'
49
+ | 'docker'
50
+ | 'rb'
51
+ | 'kt'
52
+ | 'kts'
53
+ | 'toml';
54
+
55
+ export type CodeHighlightLanguage = CanonicalLanguage;
56
+
57
+ export interface ResolvedLanguage {
58
+ canonical: CanonicalLanguage;
59
+ label: string;
60
+ }
61
+
62
+ export interface CodeHighlighterOptions {
63
+ highlight: (code: string, canonicalLanguage: CanonicalLanguage) => string;
64
+ now?: () => number;
65
+ maxBudgetMs?: number;
66
+ windowMs?: number;
67
+ }
68
+
69
+ export interface CodeHighlighter {
70
+ renderCodeBlock(code: string, rawTag?: string): string;
71
+ cache: CodeHighlightCache;
72
+ resolveLanguage(rawTag?: string): ResolvedLanguage | null;
73
+ }
74
+
75
+ export const CANONICAL_LANGUAGES: readonly CanonicalLanguage[];
76
+ export const LANGUAGE_ALIASES: ReadonlyMap<string, CanonicalLanguage>;
77
+
78
+ export const MAX_HIGHLIGHT_CHARS: number;
79
+ export const MAX_HIGHLIGHT_LINES: number;
80
+ export const MAX_HIGHLIGHT_LINE_LENGTH: number;
81
+ export const MAX_CACHE_ENTRIES: number;
82
+ export const MAX_CACHE_CHARS: number;
83
+
84
+ export const DEFAULT_TIME_BUDGET_MS: number;
85
+ export const DEFAULT_TIME_WINDOW_MS: number;
86
+
87
+ export function escapeHtml(value: unknown): string;
88
+ export function escapeAttribute(value: unknown): string;
89
+ export function resolveLanguage(rawTag?: string): ResolvedLanguage | null;
90
+
91
+ export class CodeHighlightCache {
92
+ constructor(maxEntries?: number, maxChars?: number);
93
+ get(key: string): string | undefined;
94
+ set(key: string, value: string): void;
95
+ clear(): void;
96
+ readonly size: number;
97
+ readonly totalChars: number;
98
+ }
99
+
100
+ export function createCodeHighlighter(options: CodeHighlighterOptions): CodeHighlighter;
101
+ export function executeCodeBlockCopy(
102
+ button: HTMLButtonElement,
103
+ textToCopy: string | null,
104
+ copyFn: (text: string) => Promise<boolean> | boolean,
105
+ copyTimers?: WeakMap<HTMLButtonElement, number>
106
+ ): Promise<{ success: boolean; timerId: number | null }>;
107
+ export function setupCodeBlockCopyListener(
108
+ copyFn: (text: string) => Promise<boolean> | boolean
109
+ ): () => void;