@narumitw/pi-btw 0.58.1 → 0.60.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 +8 -1
- package/dist/index.ts +490 -385
- package/dist/index.ts.map +4 -4
- package/package.json +52 -53
- package/src/bring-to-main.ts +494 -547
- package/src/btw.ts +803 -856
- package/src/fullscreen-ui.ts +673 -674
- package/src/keybindings.ts +228 -270
- package/src/main-tree-picker.ts +309 -318
- package/src/menu.ts +645 -454
- package/src/settings.ts +208 -219
- package/src/side-thread.ts +173 -201
- package/src/text.ts +21 -23
- package/src/transcript-markdown.ts +65 -0
- package/src/transcript-pager.ts +611 -662
package/src/side-thread.ts
CHANGED
|
@@ -1,221 +1,197 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
Api,
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
Context,
|
|
5
|
+
Message,
|
|
6
|
+
Model,
|
|
7
|
+
ProviderHeaders,
|
|
8
|
+
SimpleStreamOptions,
|
|
9
|
+
UserMessage,
|
|
10
10
|
} from "@earendil-works/pi-ai";
|
|
11
11
|
|
|
12
|
-
export const BTW_THINKING_LEVELS = [
|
|
13
|
-
"off",
|
|
14
|
-
"minimal",
|
|
15
|
-
"low",
|
|
16
|
-
"medium",
|
|
17
|
-
"high",
|
|
18
|
-
"xhigh",
|
|
19
|
-
"max",
|
|
20
|
-
] as const;
|
|
12
|
+
export const BTW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
21
13
|
|
|
22
14
|
export type BtwThinkingLevel = (typeof BTW_THINKING_LEVELS)[number];
|
|
23
15
|
|
|
24
16
|
export interface SideQuestionAuth {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
headers?: ProviderHeaders;
|
|
19
|
+
env?: Record<string, string>;
|
|
28
20
|
}
|
|
29
21
|
|
|
30
22
|
export type CompleteSimpleFunction = <TApi extends Api>(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
model: Model<TApi>,
|
|
24
|
+
context: Context,
|
|
25
|
+
options?: SimpleStreamOptions,
|
|
34
26
|
) => Promise<AssistantMessage>;
|
|
35
27
|
|
|
36
28
|
export type SideThreadTurn =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
29
|
+
| {
|
|
30
|
+
kind: "answered";
|
|
31
|
+
question: string;
|
|
32
|
+
answer: string;
|
|
33
|
+
response: AssistantMessage;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
kind: "error";
|
|
37
|
+
question: string;
|
|
38
|
+
answer: string;
|
|
39
|
+
};
|
|
48
40
|
|
|
49
41
|
export interface SideThread {
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
conversationContext: string;
|
|
43
|
+
turns: SideThreadTurn[];
|
|
52
44
|
}
|
|
53
45
|
|
|
54
46
|
export function createSideThread(conversationContext: string): SideThread {
|
|
55
|
-
|
|
47
|
+
return { conversationContext, turns: [] };
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
export function buildSideThreadMessages(thread: SideThread, question: string): Message[] {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
const answeredTurns = thread.turns.filter(
|
|
52
|
+
(turn): turn is Extract<SideThreadTurn, { kind: "answered" }> => turn.kind === "answered",
|
|
53
|
+
);
|
|
54
|
+
const messages: Message[] = [];
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
if (answeredTurns.length === 0) {
|
|
57
|
+
messages.push(createUserMessage(buildUserPrompt(question, thread.conversationContext)));
|
|
58
|
+
return messages;
|
|
59
|
+
}
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
messages.push(createUserMessage(buildFollowUpPrompt(question)));
|
|
78
|
-
return messages;
|
|
61
|
+
const [first, ...rest] = answeredTurns;
|
|
62
|
+
messages.push(createUserMessage(buildUserPrompt(first.question, thread.conversationContext)), first.response);
|
|
63
|
+
for (const turn of rest) {
|
|
64
|
+
messages.push(createUserMessage(buildFollowUpPrompt(turn.question)), turn.response);
|
|
65
|
+
}
|
|
66
|
+
messages.push(createUserMessage(buildFollowUpPrompt(question)));
|
|
67
|
+
return messages;
|
|
79
68
|
}
|
|
80
69
|
|
|
81
70
|
export interface CompleteSideThreadTurnOptions {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
71
|
+
thread: SideThread;
|
|
72
|
+
model: Model<Api>;
|
|
73
|
+
question: string;
|
|
74
|
+
thinkingLevel: BtwThinkingLevel;
|
|
75
|
+
auth: SideQuestionAuth;
|
|
76
|
+
signal?: AbortSignal;
|
|
77
|
+
completeSimple: CompleteSimpleFunction;
|
|
78
|
+
sessionId?: string;
|
|
90
79
|
}
|
|
91
80
|
|
|
92
81
|
export type CompleteSideThreadTurnResult =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
82
|
+
| { kind: "answered"; response: AssistantMessage; answer: string }
|
|
83
|
+
| { kind: "aborted" }
|
|
84
|
+
| { kind: "error"; message: string };
|
|
96
85
|
|
|
97
86
|
export async function completeSideThreadTurn({
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
87
|
+
thread,
|
|
88
|
+
model,
|
|
89
|
+
question,
|
|
90
|
+
thinkingLevel,
|
|
91
|
+
auth,
|
|
92
|
+
signal,
|
|
93
|
+
completeSimple,
|
|
94
|
+
sessionId,
|
|
106
95
|
}: CompleteSideThreadTurnOptions): Promise<CompleteSideThreadTurnResult> {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
96
|
+
if (signal?.aborted) return { kind: "aborted" };
|
|
97
|
+
try {
|
|
98
|
+
const response = await completeSimple(
|
|
99
|
+
model,
|
|
100
|
+
{ systemPrompt: SYSTEM_PROMPT, messages: buildSideThreadMessages(thread, question) },
|
|
101
|
+
buildStreamOptions(auth, { thinkingLevel, signal, model, sessionId }),
|
|
102
|
+
);
|
|
103
|
+
if (signal?.aborted || response?.stopReason === "aborted") return { kind: "aborted" };
|
|
104
|
+
if (!isAssistantMessage(response)) {
|
|
105
|
+
return { kind: "error", message: "The side model returned a malformed response." };
|
|
106
|
+
}
|
|
107
|
+
if (response.stopReason === "error") {
|
|
108
|
+
return {
|
|
109
|
+
kind: "error",
|
|
110
|
+
message: response.errorMessage ?? "The side model returned an error.",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
124
113
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
114
|
+
const answer = extractAssistantText(response) || "No response received.";
|
|
115
|
+
thread.turns.push({ kind: "answered", question, answer, response });
|
|
116
|
+
return { kind: "answered", response, answer };
|
|
117
|
+
} catch (error: unknown) {
|
|
118
|
+
if (signal?.aborted) return { kind: "aborted" };
|
|
119
|
+
return { kind: "error", message: formatError(error) };
|
|
120
|
+
}
|
|
132
121
|
}
|
|
133
122
|
|
|
134
123
|
export interface CompleteSideQuestionOptions {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
124
|
+
model: Model<Api>;
|
|
125
|
+
question: string;
|
|
126
|
+
conversationContext: string;
|
|
127
|
+
thinkingLevel: BtwThinkingLevel;
|
|
128
|
+
auth: SideQuestionAuth;
|
|
129
|
+
signal?: AbortSignal;
|
|
130
|
+
completeSimple: CompleteSimpleFunction;
|
|
131
|
+
sessionId?: string;
|
|
143
132
|
}
|
|
144
133
|
|
|
145
134
|
export async function completeSideQuestion({
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
135
|
+
model,
|
|
136
|
+
question,
|
|
137
|
+
conversationContext,
|
|
138
|
+
thinkingLevel,
|
|
139
|
+
auth,
|
|
140
|
+
signal,
|
|
141
|
+
completeSimple,
|
|
142
|
+
sessionId,
|
|
154
143
|
}: CompleteSideQuestionOptions): Promise<AssistantMessage> {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
144
|
+
return completeSimple(
|
|
145
|
+
model,
|
|
146
|
+
{
|
|
147
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
148
|
+
messages: [createUserMessage(buildUserPrompt(question, conversationContext))],
|
|
149
|
+
},
|
|
150
|
+
buildStreamOptions(auth, { thinkingLevel, signal, model, sessionId }),
|
|
151
|
+
);
|
|
163
152
|
}
|
|
164
153
|
|
|
165
154
|
export function extractAssistantText(response: AssistantMessage): string {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.map((content) => content.text)
|
|
175
|
-
.join("\n")
|
|
176
|
-
.trim();
|
|
155
|
+
return response.content
|
|
156
|
+
.filter(
|
|
157
|
+
(content): content is { type: "text"; text: string } =>
|
|
158
|
+
content !== null && typeof content === "object" && content.type === "text" && typeof content.text === "string",
|
|
159
|
+
)
|
|
160
|
+
.map((content) => content.text)
|
|
161
|
+
.join("\n")
|
|
162
|
+
.trim();
|
|
177
163
|
}
|
|
178
164
|
|
|
179
165
|
function isAssistantMessage(value: unknown): value is AssistantMessage {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
candidate.role === "assistant" &&
|
|
184
|
-
Array.isArray(candidate.content) &&
|
|
185
|
-
typeof candidate.stopReason === "string"
|
|
186
|
-
);
|
|
166
|
+
if (value === null || typeof value !== "object") return false;
|
|
167
|
+
const candidate = value as Partial<AssistantMessage>;
|
|
168
|
+
return candidate.role === "assistant" && Array.isArray(candidate.content) && typeof candidate.stopReason === "string";
|
|
187
169
|
}
|
|
188
170
|
|
|
189
171
|
export function buildUserPrompt(question: string, conversationContext: string): string {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
172
|
+
return [
|
|
173
|
+
"Answer this side question without modifying the main conversation.",
|
|
174
|
+
"",
|
|
175
|
+
"<side_question>",
|
|
176
|
+
question,
|
|
177
|
+
"</side_question>",
|
|
178
|
+
"",
|
|
179
|
+
"<conversation_context>",
|
|
180
|
+
conversationContext || "No prior conversation context was available.",
|
|
181
|
+
"</conversation_context>",
|
|
182
|
+
].join("\n");
|
|
201
183
|
}
|
|
202
184
|
|
|
203
185
|
export function buildFollowUpPrompt(question: string): string {
|
|
204
|
-
|
|
205
|
-
"Continue the same side conversation.",
|
|
206
|
-
"",
|
|
207
|
-
"<side_question>",
|
|
208
|
-
question,
|
|
209
|
-
"</side_question>",
|
|
210
|
-
].join("\n");
|
|
186
|
+
return ["Continue the same side conversation.", "", "<side_question>", question, "</side_question>"].join("\n");
|
|
211
187
|
}
|
|
212
188
|
|
|
213
189
|
function createUserMessage(text: string): UserMessage {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
190
|
+
return {
|
|
191
|
+
role: "user",
|
|
192
|
+
content: [{ type: "text", text }],
|
|
193
|
+
timestamp: Date.now(),
|
|
194
|
+
};
|
|
219
195
|
}
|
|
220
196
|
|
|
221
197
|
// Minimal session-headers fork of Pi core provider-attribution
|
|
@@ -227,62 +203,58 @@ function createUserMessage(text: string): UserMessage {
|
|
|
227
203
|
const OPENCODE_HOST = "opencode.ai";
|
|
228
204
|
|
|
229
205
|
function matchesOpencodeHost(baseUrl: string | undefined): boolean {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
206
|
+
if (!baseUrl) return false;
|
|
207
|
+
try {
|
|
208
|
+
return new URL(baseUrl).hostname === OPENCODE_HOST;
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
236
212
|
}
|
|
237
213
|
|
|
238
214
|
function getOpencodeSessionHeaders(
|
|
239
|
-
|
|
240
|
-
|
|
215
|
+
model: Pick<Model<Api>, "provider" | "baseUrl">,
|
|
216
|
+
sessionId?: string,
|
|
241
217
|
): ProviderHeaders | undefined {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
) {
|
|
248
|
-
return undefined;
|
|
249
|
-
}
|
|
250
|
-
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
|
218
|
+
if (!sessionId) return undefined;
|
|
219
|
+
if (model.provider !== "opencode" && model.provider !== "opencode-go" && !matchesOpencodeHost(model.baseUrl)) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
|
251
223
|
}
|
|
252
224
|
|
|
253
225
|
function mergeSessionHeaders(
|
|
254
|
-
|
|
255
|
-
|
|
226
|
+
authHeaders: ProviderHeaders | undefined,
|
|
227
|
+
sessionHeaders: ProviderHeaders | undefined,
|
|
256
228
|
): ProviderHeaders | undefined {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
229
|
+
if (!sessionHeaders && !authHeaders) return undefined;
|
|
230
|
+
// Bug-compatible with core mergeProviderAttributionHeaders: case-sensitive assign.
|
|
231
|
+
return { ...sessionHeaders, ...authHeaders };
|
|
260
232
|
}
|
|
261
233
|
|
|
262
234
|
interface BuildSideThreadStreamOptions {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
235
|
+
thinkingLevel: BtwThinkingLevel;
|
|
236
|
+
signal?: AbortSignal;
|
|
237
|
+
model?: Pick<Model<Api>, "provider" | "baseUrl">;
|
|
238
|
+
sessionId?: string;
|
|
267
239
|
}
|
|
268
240
|
|
|
269
241
|
function buildStreamOptions(
|
|
270
|
-
|
|
271
|
-
|
|
242
|
+
auth: SideQuestionAuth,
|
|
243
|
+
{ thinkingLevel, signal, model, sessionId }: BuildSideThreadStreamOptions,
|
|
272
244
|
): SimpleStreamOptions {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
245
|
+
const sessionHeaders = model ? getOpencodeSessionHeaders(model, sessionId) : undefined;
|
|
246
|
+
const options: SimpleStreamOptions = {
|
|
247
|
+
apiKey: auth.apiKey,
|
|
248
|
+
headers: mergeSessionHeaders(auth.headers, sessionHeaders),
|
|
249
|
+
env: auth.env,
|
|
250
|
+
signal,
|
|
251
|
+
};
|
|
252
|
+
if (thinkingLevel !== "off") options.reasoning = thinkingLevel;
|
|
253
|
+
return options;
|
|
282
254
|
}
|
|
283
255
|
|
|
284
256
|
function formatError(error: unknown): string {
|
|
285
|
-
|
|
257
|
+
return error instanceof Error ? error.message : String(error);
|
|
286
258
|
}
|
|
287
259
|
|
|
288
260
|
const SYSTEM_PROMPT = `You answer quick side questions for a coding-agent user.
|
package/src/text.ts
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
export function sanitizeSingleLine(text: string): string {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
return [...text.replace(/[\r\n\t]/gu, " ")]
|
|
3
|
+
.filter((character) => {
|
|
4
|
+
const code = character.charCodeAt(0);
|
|
5
|
+
return code > 31 && (code < 127 || code > 159);
|
|
6
|
+
})
|
|
7
|
+
.join("")
|
|
8
|
+
.replace(/ +/gu, " ")
|
|
9
|
+
.trim();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function formatKeyLabel(key: string): string {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
})
|
|
27
|
-
.join("+");
|
|
13
|
+
const sanitized = sanitizeSingleLine(key);
|
|
14
|
+
if (!sanitized) return "";
|
|
15
|
+
return sanitized
|
|
16
|
+
.split("+")
|
|
17
|
+
.map((part) => {
|
|
18
|
+
const lower = part.toLowerCase();
|
|
19
|
+
if (lower === "shift") return "Shift";
|
|
20
|
+
if (lower === "ctrl") return "Ctrl";
|
|
21
|
+
if (lower === "alt") return "Alt";
|
|
22
|
+
if (lower === "super") return "Super";
|
|
23
|
+
return part.length === 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`;
|
|
24
|
+
})
|
|
25
|
+
.join("+");
|
|
28
26
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { MarkdownTransformer, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SideThreadTurn } from "./side-thread.js";
|
|
3
|
+
|
|
4
|
+
export type BtwMarkdownTransformers = (theme: Theme) => readonly MarkdownTransformer[];
|
|
5
|
+
|
|
6
|
+
type MermaidMarkdownModule = typeof import("@narumitw/pi-tui-kit/markdown");
|
|
7
|
+
|
|
8
|
+
const MERMAID_MARKDOWN_MODULE = "@narumitw/pi-tui-kit/markdown";
|
|
9
|
+
const noMarkdownTransformers: BtwMarkdownTransformers = () => [];
|
|
10
|
+
|
|
11
|
+
export function prepareBtwTranscriptMarkdown(
|
|
12
|
+
turns: readonly SideThreadTurn[],
|
|
13
|
+
pendingQuestion?: string,
|
|
14
|
+
): Promise<BtwMarkdownTransformers>;
|
|
15
|
+
export function prepareBtwTranscriptMarkdown(
|
|
16
|
+
turns: readonly SideThreadTurn[],
|
|
17
|
+
pendingQuestion: string | undefined,
|
|
18
|
+
signal: AbortSignal,
|
|
19
|
+
): Promise<BtwMarkdownTransformers | undefined>;
|
|
20
|
+
export async function prepareBtwTranscriptMarkdown(
|
|
21
|
+
turns: readonly SideThreadTurn[],
|
|
22
|
+
pendingQuestion?: string,
|
|
23
|
+
signal?: AbortSignal,
|
|
24
|
+
): Promise<BtwMarkdownTransformers | undefined> {
|
|
25
|
+
const documents = turns.flatMap((turn) =>
|
|
26
|
+
turn.kind === "answered" ? [turn.question, turn.answer] : [turn.question],
|
|
27
|
+
);
|
|
28
|
+
if (pendingQuestion) documents.push(pendingQuestion);
|
|
29
|
+
if (!documents.some((document) => /mermaid/iu.test(document))) return noMarkdownTransformers;
|
|
30
|
+
if (signal?.aborted) return undefined;
|
|
31
|
+
|
|
32
|
+
const markdownModule = await settleUnlessAborted(
|
|
33
|
+
import(MERMAID_MARKDOWN_MODULE) as Promise<MermaidMarkdownModule>,
|
|
34
|
+
signal,
|
|
35
|
+
);
|
|
36
|
+
if (!markdownModule || signal?.aborted) return undefined;
|
|
37
|
+
const { createMermaidMarkdownTransformer, prepareMermaidMarkdownRenderer } = markdownModule;
|
|
38
|
+
const preparations = new Set<Promise<void>>();
|
|
39
|
+
for (const document of documents) {
|
|
40
|
+
const preparation = prepareMermaidMarkdownRenderer(document);
|
|
41
|
+
if (preparation) preparations.add(preparation);
|
|
42
|
+
}
|
|
43
|
+
if (preparations.size > 0 && !(await settleUnlessAborted(Promise.all(preparations), signal))) return undefined;
|
|
44
|
+
if (signal?.aborted) return undefined;
|
|
45
|
+
|
|
46
|
+
return (theme) => {
|
|
47
|
+
const transformer = createMermaidMarkdownTransformer(theme);
|
|
48
|
+
return transformer ? [transformer] : [];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function settleUnlessAborted<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T | undefined> {
|
|
53
|
+
if (!signal) return operation;
|
|
54
|
+
let onAbort: (() => void) | undefined;
|
|
55
|
+
const aborted = new Promise<undefined>((resolve) => {
|
|
56
|
+
onAbort = () => resolve(undefined);
|
|
57
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
58
|
+
if (signal.aborted) onAbort();
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
return await Promise.race([operation, aborted]);
|
|
62
|
+
} finally {
|
|
63
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
64
|
+
}
|
|
65
|
+
}
|