letagents 0.12.34 → 0.12.36
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 +0 -2
- package/dist/mcp/server/runtime/messages.js +18 -1
- package/dist/mcp/server/runtime/workspace-capture.js +16 -5
- package/dist/mcp/server/tools/supervised-room-turn.js +1 -1
- package/package.json +5 -3
- package/shared/activation-routing.d.mts +21 -3
- package/shared/activation-routing.mjs +37 -0
- package/shared/code-block.css +193 -0
- package/shared/code-highlighting.d.mts +109 -0
- package/shared/code-highlighting.mjs +345 -0
- package/shared/codex-owner-isolation.mjs +5 -3
- package/shared/codex-read-only-room-tools.d.mts +2 -0
- package/shared/codex-read-only-room-tools.mjs +24 -0
- package/shared/mcp-workspace-capture-worker.mjs +2 -1
- package/shared/message-link-preview-store.d.mts +10 -0
- package/shared/message-link-preview-store.mjs +77 -0
- package/shared/message-link-previews.d.mts +17 -0
- package/shared/message-link-previews.mjs +76 -0
- package/shared/message-pin-store.d.mts +20 -0
- package/shared/message-pin-store.mjs +102 -0
- package/shared/message-pins.d.mts +25 -0
- package/shared/message-pins.mjs +16 -0
- package/shared/message-reaction-store.d.mts +51 -0
- package/shared/message-reaction-store.mjs +290 -0
- package/shared/message-reactions.d.mts +73 -0
- package/shared/message-reactions.mjs +130 -0
- package/shared/message-search-controller.d.mts +38 -0
- package/shared/message-search-controller.mjs +99 -0
- package/shared/message-search.d.mts +46 -0
- package/shared/message-search.mjs +114 -0
- package/shared/room-message-urls.d.mts +25 -0
- package/shared/room-message-urls.mjs +119 -0
- package/shared/room-notification-client.d.mts +18 -0
- package/shared/room-notification-client.mjs +140 -0
- package/shared/room-notification-preferences.d.mts +17 -0
- package/shared/room-notification-preferences.mjs +34 -0
- package/shared/room-resource-invalidation.d.mts +5 -0
- package/shared/room-resource-invalidation.mjs +6 -0
- package/shared/room-settings.d.mts +16 -0
- package/shared/room-settings.mjs +14 -0
- package/shared/room-typing.d.mts +18 -0
- package/shared/room-typing.mjs +129 -0
- package/shared/room-unread-client.ts +128 -0
- package/shared/room-unread.d.mts +25 -0
- package/shared/room-unread.mjs +57 -0
- package/shared/room-unread.test.mjs +91 -0
- package/shared/rooms/RoomsDirectory.vue +1 -1
- package/shared/rooms/directory.ts +1 -0
- package/shared/slash-commands.d.mts +31 -0
- package/shared/slash-commands.mjs +48 -0
- package/shared/slash-commands.test.mjs +26 -0
- package/shared/ui/GitHubEventCard.vue +229 -0
- package/shared/ui/MessageReactionBar.vue +65 -0
- package/shared/ui/MessageReactionPicker.vue +291 -0
- package/shared/ui/MessageSearchResults.vue +106 -0
- package/shared/ui/PinnedMessages.vue +129 -0
- package/shared/ui/TypingIndicator.vue +134 -0
- package/shared/ui/message-reactions.css +254 -0
- package/shared/ui/message-search.css +186 -0
- package/shared/ui/room-agent-work.css +178 -0
- package/shared/ui/room-message-motion.ts +143 -0
- package/shared/ui/useComposerSlashCommands.ts +92 -0
- package/shared/ui/usePinnedMessageVisibility.ts +20 -0
- package/shared/ui/useRoomMessageMotion.ts +195 -0
- package/shared/ui/useSlidingTabIndicator.ts +72 -0
- package/shared/workspace-change-capture.mjs +3 -1
- package/shared/workspace-turn-capture.d.mts +4 -1
- package/shared/workspace-turn-capture.mjs +203 -6
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:
|
|
@@ -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 { promisify } from 'node:util';
|
|
|
6
6
|
import { Worker } from 'node:worker_threads';
|
|
7
7
|
import { parseRoomAgentWorkSummary } from '../../../../shared/room-agent-work.mjs';
|
|
8
8
|
import { REVIEW_LIMIT, REVIEW_PAGE_SIZE } from '../../../../shared/workspace-review.mjs';
|
|
9
|
-
import { releaseWorkspaceTree } from '../../../../shared/workspace-turn-capture.mjs';
|
|
9
|
+
import { captureRemoteTips, releaseWorkspaceTree } from '../../../../shared/workspace-turn-capture.mjs';
|
|
10
10
|
import { getLocalStatePath, readLocalStateSnapshot, updateLocalState } from '../../local-state/storage.js';
|
|
11
11
|
import { assertWorkerConnection } from '../../worker-call-context.js';
|
|
12
12
|
import { encodeRoomIdPath } from '../../room-id.js';
|
|
@@ -18,6 +18,8 @@ import { requireValidWorkerBearerRuntime } from './worker-bearer.js';
|
|
|
18
18
|
const execute = promisify(execFile);
|
|
19
19
|
const activeCalls = new Set();
|
|
20
20
|
let captureWorkerActive = false;
|
|
21
|
+
// What the remotes held when each capture began. Memory only: after a restart the capture is compared plainly.
|
|
22
|
+
const remoteTips = new Map();
|
|
21
23
|
const validId = (id) => /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(id);
|
|
22
24
|
const directory = (id) => join(`${getLocalStatePath()}.workspaces`, id);
|
|
23
25
|
const preparationDirectory = (capture) => {
|
|
@@ -70,7 +72,7 @@ async function exclusive(key, operation) {
|
|
|
70
72
|
activeCalls.delete(key);
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
|
-
async function runCapture(operation, capture, text) {
|
|
75
|
+
async function runCapture(operation, capture, text, tips = null) {
|
|
74
76
|
if (captureWorkerActive)
|
|
75
77
|
throw new Error('Another workspace is being captured. Retry shortly; the original baseline is preserved.');
|
|
76
78
|
captureWorkerActive = true;
|
|
@@ -78,7 +80,7 @@ async function runCapture(operation, capture, text) {
|
|
|
78
80
|
try {
|
|
79
81
|
return await new Promise((resolve, reject) => {
|
|
80
82
|
worker = new Worker(new URL('../../../../shared/mcp-workspace-capture-worker.mjs', import.meta.url), {
|
|
81
|
-
execArgv: [], workerData: { operation, capture, text, directory: operation === 'finish' ? preparationDirectory(capture) : null },
|
|
83
|
+
execArgv: [], workerData: { operation, capture, text, remoteTips: tips, directory: operation === 'finish' ? preparationDirectory(capture) : null },
|
|
82
84
|
});
|
|
83
85
|
const timeout = setTimeout(() => { reject(new Error('Capture exceeded its time limit. Retry using the same capture_id.')); void worker?.terminate(); }, 120_000);
|
|
84
86
|
worker.once('message', result => { clearTimeout(timeout); result.error ? reject(new Error(result.error)) : resolve(result); });
|
|
@@ -111,6 +113,10 @@ export async function beginWorkspaceCapture(input) {
|
|
|
111
113
|
instruction: 'Continue this capture. Keep its capture_id and call publish_workspace_capture when finished.',
|
|
112
114
|
...(prior.baseline ? {} : { warning: 'The starting snapshot is unavailable. Exact changes for this piece of work cannot be attributed.' }) };
|
|
113
115
|
}
|
|
116
|
+
// Asked before the work starts, and not awaited: a slow remote must not delay it. The remotes
|
|
117
|
+
// are the ones this clone configures: nothing outside the clone records where it came from,
|
|
118
|
+
// and an independent worker reports its own work. (The supervisor daemon asks its own record.)
|
|
119
|
+
const tips = captureRemoteTips(workspace);
|
|
114
120
|
const baseRevision = await git(['rev-parse', '--verify', 'HEAD']);
|
|
115
121
|
const capture = { capture_id: randomUUID(), api_url: getApiUrl(), room_id: session.room_id,
|
|
116
122
|
agent_key: session.agent_key, agent_instance_id: session.agent_instance_id, workspace, base_revision: baseRevision,
|
|
@@ -125,8 +131,11 @@ export async function beginWorkspaceCapture(input) {
|
|
|
125
131
|
delete records[id];
|
|
126
132
|
records[capture.capture_id] = capture;
|
|
127
133
|
});
|
|
128
|
-
for (const record of obsolete)
|
|
134
|
+
for (const record of obsolete) {
|
|
135
|
+
remoteTips.delete(record.capture_id);
|
|
129
136
|
await rm(directory(record.capture_id), { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
remoteTips.set(capture.capture_id, tips);
|
|
130
139
|
try {
|
|
131
140
|
const result = await runCapture('begin', capture);
|
|
132
141
|
capture.baseline = result.baseline ?? null;
|
|
@@ -176,7 +185,7 @@ export async function publishWorkspaceCapture(input) {
|
|
|
176
185
|
throw new Error('Supply a short summary of at most 400 characters.');
|
|
177
186
|
const candidate = { ...capture, preparation_id: randomUUID() };
|
|
178
187
|
try {
|
|
179
|
-
await runCapture('finish', candidate, text);
|
|
188
|
+
await runCapture('finish', candidate, text, await remoteTips.get(capture.capture_id) ?? null);
|
|
180
189
|
await preparedCapture(candidate);
|
|
181
190
|
// Commit one immutable preparation under the same lock that fences
|
|
182
191
|
// reconnects. Stale processes only write their own disposable directory.
|
|
@@ -247,6 +256,7 @@ export async function publishWorkspaceCapture(input) {
|
|
|
247
256
|
if (capture.next_page === total) {
|
|
248
257
|
capture = { ...capture, phase: 'published' };
|
|
249
258
|
persist();
|
|
259
|
+
remoteTips.delete(capture.capture_id);
|
|
250
260
|
await rm(directory(capture.capture_id), { recursive: true, force: true });
|
|
251
261
|
}
|
|
252
262
|
return { status: capture.phase === 'published' ? 'published' : 'uploading', capture_id: capture.capture_id,
|
|
@@ -259,6 +269,7 @@ export async function publishWorkspaceCapture(input) {
|
|
|
259
269
|
if (error instanceof ApiError && error.status === 410) {
|
|
260
270
|
capture = { ...capture, phase: 'blocked' };
|
|
261
271
|
persist();
|
|
272
|
+
remoteTips.delete(capture.capture_id);
|
|
262
273
|
await rm(directory(capture.capture_id), { recursive: true, force: true });
|
|
263
274
|
}
|
|
264
275
|
throw error;
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
const MAX_ROOM_REPLY_BYTES = 32 * 1024;
|
|
3
3
|
/** Select routing only; the daemon remains the sole final-answer publisher. */
|
|
4
4
|
export function registerReplyThreadTool(server) {
|
|
5
|
-
const tool = server.tool("set_reply_thread", "
|
|
5
|
+
const tool = server.tool("set_reply_thread", "Opt into a new thread for your final answer only when the user asks for one or a sustained side discussion would distract from the room. Answer ordinary top-level questions in the main room by default, without this tool; existing thread replies stay threaded automatically. Takes no arguments, sends no message, and does not finish the turn. Continue working and use your normal final completion when ready. Unavailable for synthetic task continuations or corrections.", {}, async () => { throw new Error("set_reply_thread requires the active daemon-owned room turn."); });
|
|
6
6
|
// The legacy raw-shape overload strips unknown keys. Preserve a strict schema
|
|
7
7
|
// so a caller cannot mistake ignored room/recipient/text arguments for authority.
|
|
8
8
|
tool.inputSchema = z.object({}).strict();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letagents",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.36",
|
|
4
4
|
"description": "Let Agents Chat \u2014 MCP server for AI agent communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/mcp/server.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"license": "BUSL-1.1",
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@modelcontextprotocol/sdk": "1.
|
|
53
|
+
"@modelcontextprotocol/sdk": "1.31.0",
|
|
54
54
|
"drizzle-orm": "0.45.2",
|
|
55
55
|
"express": "5.2.1",
|
|
56
56
|
"pg": "8.20.0",
|
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
},
|
|
72
72
|
"fast-uri": "3.1.8",
|
|
73
73
|
"hono": "4.13.8",
|
|
74
|
-
"ip-address": "10.7.2"
|
|
74
|
+
"ip-address": "10.7.2",
|
|
75
|
+
"@modelcontextprotocol/sdk": "1.31.0",
|
|
76
|
+
"proxy-addr": "2.0.8"
|
|
75
77
|
}
|
|
76
78
|
}
|
|
@@ -18,13 +18,33 @@ export declare function humanConversationFallback(input: {
|
|
|
18
18
|
* channel for a managed local worker.
|
|
19
19
|
*/
|
|
20
20
|
export declare function isUntrustedExternalActivationSource(source: unknown): boolean;
|
|
21
|
+
export interface AgentMessageReplyTurn {
|
|
22
|
+
/** 1-based position in the reply order; only positions 2 and later are sent. */
|
|
23
|
+
position: number;
|
|
24
|
+
count: number;
|
|
25
|
+
/** Labels of the agents before this one that answered, in turn order. */
|
|
26
|
+
prior_speakers: string[];
|
|
27
|
+
}
|
|
21
28
|
export interface AgentMessageActivation {
|
|
22
29
|
for_current_agent: {
|
|
23
30
|
decision: AgentMessageActivationDecision;
|
|
24
31
|
reason: AgentMessageActivationReason;
|
|
25
32
|
addressed: boolean;
|
|
33
|
+
turn?: AgentMessageReplyTurn;
|
|
34
|
+
guidance?: string;
|
|
26
35
|
};
|
|
27
36
|
}
|
|
37
|
+
/** A receipt as the activation authority sees it. Turn fields are optional. */
|
|
38
|
+
export type AgentMessageActivationReceipt = {
|
|
39
|
+
activation_reason: string;
|
|
40
|
+
turn_position?: number | null;
|
|
41
|
+
turn_count?: number | null;
|
|
42
|
+
prior_speakers?: readonly string[] | null;
|
|
43
|
+
/** Why the held turn was released: "turn", "deadline", "activation" or "skipped". */
|
|
44
|
+
hold_release_reason?: string | null;
|
|
45
|
+
};
|
|
46
|
+
/** The turn note for an agent that answers a shared message after others. */
|
|
47
|
+
export declare function replyTurnGuidance(position: number, count: number, priorSpeakers?: readonly string[], releaseReason?: string | null): string;
|
|
28
48
|
type MessageLike = {
|
|
29
49
|
id?: unknown;
|
|
30
50
|
sender?: unknown;
|
|
@@ -85,9 +105,7 @@ export declare function attachAgentMessageActivation<T extends MessageLike>(mess
|
|
|
85
105
|
* Only messages that predate routing snapshots keep the lazy per-reader
|
|
86
106
|
* decision, so legacy backlog mentions still activate rotated sessions.
|
|
87
107
|
*/
|
|
88
|
-
export declare function attachAgentMessageActivationsFromReceipts<T extends MessageLike>(messages: readonly T[], identity: ActivationIdentity | null, receiptsMap: ReadonlyMap<number | string, {
|
|
89
|
-
activation_reason: string;
|
|
90
|
-
}>, snapshotNumbers: ReadonlySet<number>, context?: AgentMessageActivationContext): T[] | Array<T & {
|
|
108
|
+
export declare function attachAgentMessageActivationsFromReceipts<T extends MessageLike>(messages: readonly T[], identity: ActivationIdentity | null, receiptsMap: ReadonlyMap<number | string, AgentMessageActivationReceipt>, snapshotNumbers: ReadonlySet<number>, context?: AgentMessageActivationContext): T[] | Array<T & {
|
|
91
109
|
activation: AgentMessageActivation;
|
|
92
110
|
}>;
|
|
93
111
|
export declare function attachAgentMessageActivations<T extends MessageLike>(messages: readonly T[], identity: ActivationIdentity | null, context?: AgentMessageActivationContext): T[] | Array<T & {
|
|
@@ -80,6 +80,7 @@ export function attachAgentMessageActivationsFromReceipts(messages, identity, re
|
|
|
80
80
|
decision: "activate",
|
|
81
81
|
reason: reason || "explicit_mention",
|
|
82
82
|
addressed: true,
|
|
83
|
+
...replyTurnActivationFields(receipt),
|
|
83
84
|
},
|
|
84
85
|
},
|
|
85
86
|
};
|
|
@@ -103,6 +104,42 @@ export function attachAgentMessageActivationsFromReceipts(messages, identity, re
|
|
|
103
104
|
return attachAgentMessageActivation(message, identity, context);
|
|
104
105
|
});
|
|
105
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* The turn note for an agent that answers a shared message after others.
|
|
109
|
+
* Desktop adapters print `for_current_agent` into the prompt verbatim, so this
|
|
110
|
+
* text reaches the model without a client change.
|
|
111
|
+
*/
|
|
112
|
+
export function replyTurnGuidance(position, count, priorSpeakers = [], releaseReason = null) {
|
|
113
|
+
const opening = `Turn order: you answer this message in position ${position} of ${count}.`;
|
|
114
|
+
const speakers = priorSpeakers.filter((speaker) => typeof speaker === "string" && speaker.trim());
|
|
115
|
+
if (speakers.length === 0) {
|
|
116
|
+
// Say "did not answer in time" only when that is what happened. A
|
|
117
|
+
// turn opened by a direct activation, or after agents that ended with
|
|
118
|
+
// nothing to say, may still have earlier agents at work.
|
|
119
|
+
return releaseReason === "deadline"
|
|
120
|
+
? `${opening} The agent before you did not answer in time. Answer now.`
|
|
121
|
+
: `${opening} The agents before you have not answered yet. Cover only what they are unlikely to say.`;
|
|
122
|
+
}
|
|
123
|
+
const names = speakers.length === 1
|
|
124
|
+
? speakers[0]
|
|
125
|
+
: `${speakers.slice(0, -1).join(", ")} and ${speakers.at(-1)}`;
|
|
126
|
+
const replies = speakers.length === 1 ? "their reply is" : "their replies are";
|
|
127
|
+
return `${opening} ${names} answered before you; ${replies} in the recent room context.`
|
|
128
|
+
+ " Do not repeat their points. Add only new points, agree in one short line, or disagree and give the reason.";
|
|
129
|
+
}
|
|
130
|
+
/** Position 1 and parallel receipts add nothing; later positions add turn and guidance. */
|
|
131
|
+
function replyTurnActivationFields(receipt) {
|
|
132
|
+
const position = Number(receipt.turn_position);
|
|
133
|
+
const count = Number(receipt.turn_count);
|
|
134
|
+
if (!Number.isSafeInteger(position) || !Number.isSafeInteger(count) || position < 2 || count < position) {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
const priorSpeakers = Array.isArray(receipt.prior_speakers) ? receipt.prior_speakers : [];
|
|
138
|
+
return {
|
|
139
|
+
turn: { position, count, prior_speakers: [...priorSpeakers] },
|
|
140
|
+
guidance: replyTurnGuidance(position, count, priorSpeakers, receipt.hold_release_reason ?? null),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
106
143
|
export function attachAgentMessageActivations(messages, identity, context = {}) {
|
|
107
144
|
if (!identity || identity.session_kind !== "worker") {
|
|
108
145
|
return [...messages];
|
|
@@ -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;
|