fix: Bedrock ARN region routing, Copilot device-flow pacing, TUI input fixes (#109740)

* build(deps): bump terminal UI library to 0.80.9

* fix(plugins): Bedrock ARN region routing, stop-reason detail, and Copilot device-flow pacing
This commit is contained in:
Peter Steinberger
2026-07-17 01:09:29 -07:00
committed by GitHub
parent 2bf07bdea5
commit e693d279b3
11 changed files with 314 additions and 46 deletions
@@ -1,5 +1,9 @@
// Amazon Bedrock tests cover stream plugin behavior.
import { BedrockRuntimeClient, ConversationRole } from "@aws-sdk/client-bedrock-runtime";
import {
BedrockRuntimeClient,
ConversationRole,
StopReason as BedrockStopReason,
} from "@aws-sdk/client-bedrock-runtime";
import { onLlmRequestActivity } from "openclaw/plugin-sdk/provider-stream-shared";
import { afterEach, describe, expect, it, vi } from "vitest";
import { streamBedrock, streamSimpleBedrock } from "./stream.runtime.js";
@@ -48,7 +52,30 @@ async function* streamEvents(events: unknown[]) {
}
}
async function captureClientRegion(
model: Parameters<typeof streamBedrock>[0],
options: Parameters<typeof streamBedrock>[2] = {},
): Promise<string> {
const send = vi.spyOn(BedrockRuntimeClient.prototype, "send").mockResolvedValue({
$metadata: { httpStatusCode: 200 },
stream: streamEvents([
{ messageStart: { role: ConversationRole.ASSISTANT } },
{ messageStop: { stopReason: BedrockStopReason.END_TURN } },
]),
} as never);
await streamBedrock(
model,
{ messages: [{ role: "user", content: "Hello", timestamp: 0 }] } as never,
options,
).result();
const client = send.mock.contexts[0] as BedrockRuntimeClient;
return client.config.region();
}
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
@@ -203,6 +230,71 @@ describe("Bedrock profile endpoint resolution", () => {
),
).toBe(false);
});
it.each([
{
name: "plain model id",
modelId: "amazon.nova-micro-v1:0",
ambientRegion: "eu-west-1",
expectedRegion: "eu-west-1",
},
{
name: "application inference-profile ARN",
modelId: "arn:aws:bedrock:us-west-2:123456789012:application-inference-profile/profile-abc",
ambientRegion: "us-east-1",
expectedRegion: "us-west-2",
},
{
name: "GovCloud inference-profile ARN",
modelId:
"arn:aws-us-gov:bedrock:us-gov-west-1:123456789012:application-inference-profile/profile-abc",
ambientRegion: "us-east-1",
expectedRegion: "us-gov-west-1",
},
{
name: "ARN with explicit region option",
modelId: "arn:aws:bedrock:us-west-2:123456789012:application-inference-profile/profile-abc",
ambientRegion: "us-east-1",
explicitRegion: "ap-southeast-2",
expectedRegion: "ap-southeast-2",
},
])(
"resolves $name to $expectedRegion",
async ({ modelId, ambientRegion, explicitRegion, expectedRegion }) => {
vi.stubEnv("AWS_REGION", ambientRegion);
await expect(
captureClientRegion(
bedrockModel({ id: modelId }),
explicitRegion ? { region: explicitRegion } : {},
),
).resolves.toBe(expectedRegion);
},
);
});
describe("Bedrock stop reasons", () => {
it.each([
BedrockStopReason.CONTENT_FILTERED,
BedrockStopReason.GUARDRAIL_INTERVENED,
BedrockStopReason.MALFORMED_MODEL_OUTPUT,
BedrockStopReason.MALFORMED_TOOL_USE,
])("reports the provider stop reason %s", async (stopReason) => {
vi.spyOn(BedrockRuntimeClient.prototype, "send").mockResolvedValue({
$metadata: { httpStatusCode: 200 },
stream: streamEvents([
{ messageStart: { role: ConversationRole.ASSISTANT } },
{ messageStop: { stopReason } },
]),
} as never);
const result = await streamBedrock(bedrockModel({}), {
messages: [{ role: "user", content: "Hello", timestamp: 0 }],
} as never).result();
expect(result.stopReason).toBe("error");
expect(result.errorMessage).toBe(stopReason);
});
});
describe("Bedrock thinking effort mapping", () => {
+29 -13
View File
@@ -166,11 +166,12 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
profile: options.profile,
};
const configuredRegion = getConfiguredBedrockRegion(options);
const requestRegion = options.region || getBedrockModelArnRegion(model.id) || configuredRegion;
const hasConfiguredProfile = hasConfiguredBedrockProfile(options);
const endpointRegion = getStandardBedrockEndpointRegion(model.baseUrl);
const useExplicitEndpoint = shouldUseExplicitBedrockEndpoint(
model.baseUrl,
configuredRegion,
requestRegion,
hasConfiguredProfile,
);
@@ -187,11 +188,10 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
// in Node.js/Bun environment only
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
// Region resolution: explicit option > env vars > SDK default chain.
// When AWS_PROFILE is set, we leave region undefined so the SDK can
// resovle it from aws profile configs. Otherwise fall back to us-east-1.
if (configuredRegion) {
config.region = configuredRegion;
// Region resolution: explicit option > model ARN > env vars > SDK default chain.
// When AWS_PROFILE is set, leave region undefined so the SDK can resolve it.
if (requestRegion) {
config.region = requestRegion;
} else if (endpointRegion && useExplicitEndpoint) {
config.region = endpointRegion;
} else if (!hasConfiguredProfile) {
@@ -220,7 +220,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
// Non-Node environment (browser): fall back to us-east-1 since
// there's no config file resolution available.
config.region =
configuredRegion ||
requestRegion ||
(endpointRegion && useExplicitEndpoint ? endpointRegion : undefined) ||
"us-east-1";
}
@@ -303,7 +303,11 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
model.provider,
);
} else {
output.stopReason = mapStopReason(item.messageStop.stopReason);
const mappedStop = mapStopReason(item.messageStop.stopReason);
output.stopReason = mappedStop.stopReason;
if (mappedStop.errorMessage) {
output.errorMessage = mappedStop.errorMessage;
}
}
} else if (item.metadata) {
handleMetadata(item.metadata, model, output);
@@ -1002,21 +1006,33 @@ function convertToolConfig(
return { tools: bedrockTools, toolChoice: bedrockToolChoice };
}
function mapStopReason(reason: string | undefined): StopReason {
function mapStopReason(reason: string | undefined): {
stopReason: StopReason;
errorMessage?: string;
} {
switch (reason) {
case BedrockStopReason.END_TURN:
case BedrockStopReason.STOP_SEQUENCE:
return "stop";
return { stopReason: "stop" };
case BedrockStopReason.MAX_TOKENS:
case BedrockStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED:
return "length";
return { stopReason: "length" };
case BedrockStopReason.TOOL_USE:
return "toolUse";
return { stopReason: "toolUse" };
case BedrockStopReason.CONTENT_FILTERED:
case BedrockStopReason.GUARDRAIL_INTERVENED:
case BedrockStopReason.MALFORMED_MODEL_OUTPUT:
case BedrockStopReason.MALFORMED_TOOL_USE:
return { stopReason: "error", errorMessage: reason };
default:
return "error";
return reason ? { stopReason: "error", errorMessage: reason } : { stopReason: "error" };
}
}
function getBedrockModelArnRegion(modelId: string): string | undefined {
return /^arn:aws(?:-[a-z0-9-]+)?:bedrock:([a-z0-9-]+):/.exec(modelId)?.[1];
}
function getConfiguredBedrockRegion(options: BedrockOptions): string | undefined {
if (typeof process === "undefined") {
return options.region;
+112 -6
View File
@@ -40,8 +40,20 @@ function guardResponse(body: unknown, status = 200, url = DEVICE_CODE_URL) {
afterEach(() => {
mocks.fetchWithSsrFGuard.mockReset();
vi.restoreAllMocks();
vi.useRealTimers();
});
function runDeviceFlowAfterFirstPoll(
io: Parameters<typeof runGitHubCopilotDeviceFlow>[0],
domain?: string,
) {
vi.useFakeTimers();
const result = runGitHubCopilotDeviceFlow(io, domain);
return Promise.all([result, vi.advanceTimersByTimeAsync(5_000)]).then(
([flowResult]) => flowResult,
);
}
describe("runGitHubCopilotDeviceFlow — normal flow", () => {
it("bounds requests and returns authorized status and access token on successful flow", async () => {
let callIdx = 0;
@@ -64,7 +76,7 @@ describe("runGitHubCopilotDeviceFlow — normal flow", () => {
});
const showCode = vi.fn(async () => {});
const result = await runGitHubCopilotDeviceFlow({ showCode, signal: controller.signal });
const result = await runDeviceFlowAfterFirstPoll({ showCode, signal: controller.signal });
expect(result).toEqual({ status: "authorized", accessToken: "ghu_tok_xyz" });
expect(showCode).toHaveBeenCalledWith({
@@ -86,7 +98,7 @@ describe("runGitHubCopilotDeviceFlow — normal flow", () => {
return guardResponse({ error: "access_denied" }, 200, ACCESS_TOKEN_URL);
});
const result = await runGitHubCopilotDeviceFlow({
const result = await runDeviceFlowAfterFirstPoll({
showCode: vi.fn(async () => {}),
});
expect(result).toEqual({ status: "access_denied" });
@@ -102,7 +114,7 @@ describe("runGitHubCopilotDeviceFlow — normal flow", () => {
return guardResponse({ error: "expired_token" }, 200, ACCESS_TOKEN_URL);
});
const result = await runGitHubCopilotDeviceFlow({
const result = await runDeviceFlowAfterFirstPoll({
showCode: vi.fn(async () => {}),
});
expect(result).toEqual({ status: "expired" });
@@ -128,10 +140,25 @@ describe("runGitHubCopilotDeviceFlow — HTTP error propagation", () => {
return guardResponse({}, 500, ACCESS_TOKEN_URL);
});
await expect(runGitHubCopilotDeviceFlow({ showCode: vi.fn(async () => {}) })).rejects.toThrow(
await expect(runDeviceFlowAfterFirstPoll({ showCode: vi.fn(async () => {}) })).rejects.toThrow(
"GitHub device token failed: HTTP 500",
);
});
it("rejects a malformed access token response", async () => {
let callIdx = 0;
mocks.fetchWithSsrFGuard.mockImplementation(async () => {
callIdx += 1;
if (callIdx === 1) {
return guardResponse(VALID_DEVICE_CODE_BODY);
}
return guardResponse({ access_token: null, token_type: "bearer" }, 200, ACCESS_TOKEN_URL);
});
await expect(runDeviceFlowAfterFirstPoll({ showCode: vi.fn(async () => {}) })).rejects.toThrow(
"GitHub device flow returned an invalid access token",
);
});
});
describe("postGitHubDeviceFlowForm — response size bound", () => {
@@ -208,7 +235,7 @@ describe("postGitHubDeviceFlowForm — response size bound", () => {
};
});
await expect(runGitHubCopilotDeviceFlow({ showCode: vi.fn(async () => {}) })).rejects.toThrow(
await expect(runDeviceFlowAfterFirstPoll({ showCode: vi.fn(async () => {}) })).rejects.toThrow(
"github-copilot.device-flow",
);
@@ -246,7 +273,7 @@ describe("runGitHubCopilotDeviceFlow — data-residency GitHub Enterprise", () =
});
const showCode = vi.fn(async () => {});
const result = await runGitHubCopilotDeviceFlow({ showCode }, GHE_DOMAIN);
const result = await runDeviceFlowAfterFirstPoll({ showCode }, GHE_DOMAIN);
expect(result).toEqual({ status: "authorized", accessToken: "ghu_ghe_tok" });
expect(urls).toEqual([gheDeviceCodeUrl, gheAccessTokenUrl]);
@@ -271,3 +298,82 @@ describe("runGitHubCopilotDeviceFlow — data-residency GitHub Enterprise", () =
).rejects.toThrow("unexpected verification URL");
});
});
describe("runGitHubCopilotDeviceFlow — polling intervals", () => {
it("waits before the first poll and keeps cumulative slow_down increases", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-16T00:00:00Z"));
const startedAt = Date.now();
const pollTimes: number[] = [];
const pollResponses = [
{ error: "authorization_pending" },
{ error: "slow_down" },
{ error: "slow_down" },
{ access_token: "test-access-token", token_type: "bearer" },
];
mocks.fetchWithSsrFGuard.mockImplementation(async (params) => {
if (params.url === DEVICE_CODE_URL) {
const { interval: _interval, ...withoutInterval } = VALID_DEVICE_CODE_BODY;
return guardResponse(withoutInterval);
}
pollTimes.push(Date.now());
return guardResponse(pollResponses.shift(), 200, ACCESS_TOKEN_URL);
});
const result = runGitHubCopilotDeviceFlow({ showCode: vi.fn(async () => {}) });
await vi.advanceTimersByTimeAsync(4_999);
expect(pollTimes).toEqual([]);
await vi.advanceTimersByTimeAsync(1);
expect(pollTimes).toEqual([startedAt + 5_000]);
await vi.advanceTimersByTimeAsync(5_000);
expect(pollTimes).toEqual([startedAt + 5_000, startedAt + 10_000]);
await vi.advanceTimersByTimeAsync(10_000);
expect(pollTimes).toEqual([startedAt + 5_000, startedAt + 10_000, startedAt + 20_000]);
await vi.advanceTimersByTimeAsync(15_000);
await expect(result).resolves.toEqual({
status: "authorized",
accessToken: "test-access-token",
});
expect(pollTimes).toEqual([
startedAt + 5_000,
startedAt + 10_000,
startedAt + 20_000,
startedAt + 35_000,
]);
});
it("uses the interval returned with slow_down", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-16T00:00:00Z"));
const startedAt = Date.now();
const pollTimes: number[] = [];
const pollResponses = [
{ error: "slow_down", interval: 7 },
{ access_token: "test-access-token", token_type: "bearer" },
];
mocks.fetchWithSsrFGuard.mockImplementation(async (params) => {
if (params.url === DEVICE_CODE_URL) {
return guardResponse({ ...VALID_DEVICE_CODE_BODY, interval: 2 });
}
pollTimes.push(Date.now());
return guardResponse(pollResponses.shift(), 200, ACCESS_TOKEN_URL);
});
const result = runGitHubCopilotDeviceFlow({ showCode: vi.fn(async () => {}) });
await vi.advanceTimersByTimeAsync(2_000);
expect(pollTimes).toEqual([startedAt + 2_000]);
await vi.advanceTimersByTimeAsync(6_999);
expect(pollTimes).toHaveLength(1);
await vi.advanceTimersByTimeAsync(1);
await expect(result).resolves.toEqual({
status: "authorized",
accessToken: "test-access-token",
});
expect(pollTimes).toEqual([startedAt + 2_000, startedAt + 9_000]);
});
});
+22 -6
View File
@@ -25,6 +25,8 @@ import {
const CLIENT_ID = "Iv1.b507a08c87ecfe98";
const GITHUB_DEVICE_FLOW_REQUEST_TIMEOUT_MS = 30_000;
const GITHUB_DEVICE_FLOW_DEFAULT_INTERVAL_MS = 5_000;
const GITHUB_DEVICE_FLOW_SLOW_DOWN_INCREMENT_MS = 5_000;
// Data-residency GitHub Enterprise support: the device flow, token exchange, and
// completions endpoints all live under the tenant host (e.g. "acme.ghe.com")
// instead of github.com. The host is threaded in from the selected auth flow so
@@ -55,6 +57,7 @@ type DeviceTokenResponse =
error: string;
error_description?: string;
error_uri?: string;
interval?: unknown;
};
const GITHUB_DEVICE_ACCESS_DENIED = Symbol("github-device-access-denied");
@@ -100,7 +103,10 @@ function parseDeviceCodeResponse(
issuedAt: number,
): DeviceCodeResponse {
const expiresInMs = positiveSecondsToSafeMilliseconds(value.expires_in);
const intervalMs = nonNegativeSecondsToSafeMilliseconds(value.interval);
const intervalMs =
value.interval === undefined
? GITHUB_DEVICE_FLOW_DEFAULT_INTERVAL_MS
: nonNegativeSecondsToSafeMilliseconds(value.interval);
const expiresAt =
expiresInMs === undefined
? undefined
@@ -199,7 +205,13 @@ async function pollForAccessToken(params: {
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
});
let intervalMs = params.intervalMs;
while (Date.now() < params.expiresAt) {
await sleepGitHubDevicePollDelay(intervalMs, params.expiresAt, params.signal);
if (Date.now() >= params.expiresAt) {
break;
}
const json = (await postGitHubDeviceFlowForm({
url: accessTokenUrl(params.domain),
body: bodyBase,
@@ -207,17 +219,21 @@ async function pollForAccessToken(params: {
domain: params.domain,
...(params.signal ? { signal: params.signal } : {}),
})) as DeviceTokenResponse;
if ("access_token" in json && typeof json.access_token === "string") {
return json.access_token;
if ("access_token" in json) {
if (typeof json.access_token === "string") {
return json.access_token;
}
throw new Error("GitHub device flow returned an invalid access token");
}
const err = "error" in json ? json.error : "unknown";
const err = json.error;
if (err === "authorization_pending") {
await sleepGitHubDevicePollDelay(params.intervalMs, params.expiresAt, params.signal);
continue;
}
if (err === "slow_down") {
await sleepGitHubDevicePollDelay(params.intervalMs + 2000, params.expiresAt, params.signal);
intervalMs =
positiveSecondsToSafeMilliseconds(json.interval) ??
Math.min(Number.MAX_SAFE_INTEGER, intervalMs + GITHUB_DEVICE_FLOW_SLOW_DOWN_INCREMENT_MS);
continue;
}
if (err === "expired_token") {
+4 -4
View File
@@ -14,7 +14,7 @@
"@anthropic-ai/sdk": "0.109.1",
"@clack/core": "1.4.2",
"@clack/prompts": "1.6.0",
"@earendil-works/pi-tui": "0.80.3",
"@earendil-works/pi-tui": "0.80.9",
"@google/genai": "2.10.0",
"@grammyjs/runner": "2.0.3",
"@grammyjs/transformer-throttler": "1.2.1",
@@ -161,9 +161,9 @@
}
},
"node_modules/@earendil-works/pi-tui": {
"version": "0.80.3",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.3.tgz",
"integrity": "sha512-2BJI6qwRQfnM0Q7seL1+SbacU/jRRjBnN7Hu3n9BjAn7/s5FaBNnvdD1qBQYRsFTHfjqMaDsjYqanPyqwXj99w==",
"version": "0.80.9",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.9.tgz",
"integrity": "sha512-unPTW8hRgIHEGjV8mJJ2jqm+fzgnRubes6V2FPk9ay1W9ZLofcpYQ3NDfrODXSci+oKbBpX9JyYUMfQV6jCA/A==",
"license": "MIT",
"dependencies": {
"get-east-asian-width": "1.6.0",
+1 -1
View File
@@ -2025,7 +2025,7 @@
"@anthropic-ai/sdk": "0.109.1",
"@clack/core": "1.4.2",
"@clack/prompts": "1.6.0",
"@earendil-works/pi-tui": "0.80.3",
"@earendil-works/pi-tui": "0.80.9",
"@google/genai": "2.10.0",
"@grammyjs/runner": "2.0.3",
"@grammyjs/transformer-throttler": "1.2.1",
+5 -5
View File
@@ -56,8 +56,8 @@ importers:
specifier: 1.6.0
version: 1.6.0
'@earendil-works/pi-tui':
specifier: 0.80.3
version: 0.80.3
specifier: 0.80.9
version: 0.80.9
'@google/genai':
specifier: 2.10.0
version: 2.10.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))
@@ -2871,8 +2871,8 @@ packages:
resolution: {integrity: sha512-3yJ255e4ag3wfZu/DSxeOZK1UtnqNxnspmLaQetGT0pDkThNZoHs+Zg6dgZZ19JEVomXygvfHn9lNpICZuYtEA==}
engines: {node: '>=22.12.0'}
'@earendil-works/pi-tui@0.80.3':
resolution: {integrity: sha512-2BJI6qwRQfnM0Q7seL1+SbacU/jRRjBnN7Hu3n9BjAn7/s5FaBNnvdD1qBQYRsFTHfjqMaDsjYqanPyqwXj99w==}
'@earendil-works/pi-tui@0.80.9':
resolution: {integrity: sha512-unPTW8hRgIHEGjV8mJJ2jqm+fzgnRubes6V2FPk9ay1W9ZLofcpYQ3NDfrODXSci+oKbBpX9JyYUMfQV6jCA/A==}
engines: {node: '>=22.19.0'}
'@emnapi/core@1.11.1':
@@ -9311,7 +9311,7 @@ snapshots:
- opusscript
- utf-8-validate
'@earendil-works/pi-tui@0.80.3':
'@earendil-works/pi-tui@0.80.9':
dependencies:
get-east-asian-width: 1.6.0
marked: 18.0.5
+9 -3
View File
@@ -1,5 +1,10 @@
// Hyperlink markdown helpers render markdown links with TUI hyperlink styling.
import type { Component, DefaultTextStyle, MarkdownTheme } from "@earendil-works/pi-tui";
import type {
Component,
DefaultTextStyle,
MarkdownOptions,
MarkdownTheme,
} from "@earendil-works/pi-tui";
import { Markdown } from "@earendil-works/pi-tui";
import { addOsc8Hyperlinks, extractUrls } from "../osc8-hyperlinks.js";
@@ -17,9 +22,10 @@ export class HyperlinkMarkdown implements Component {
paddingX: number,
paddingY: number,
theme: MarkdownTheme,
options?: DefaultTextStyle,
defaultTextStyle?: DefaultTextStyle,
options?: MarkdownOptions,
) {
this.inner = new Markdown(text, paddingX, paddingY, theme, options);
this.inner = new Markdown(text, paddingX, paddingY, theme, defaultTextStyle, options);
this.urls = extractUrls(text);
}
+9 -3
View File
@@ -4,15 +4,21 @@ import { markdownTheme } from "../theme/theme.js";
import { HyperlinkMarkdown } from "./hyperlink-markdown.js";
// Shared markdown message wrapper with a leading spacer for chat-log rows.
type MarkdownOptions = ConstructorParameters<typeof HyperlinkMarkdown>[4];
type DefaultTextStyle = ConstructorParameters<typeof HyperlinkMarkdown>[4];
type MarkdownOptions = ConstructorParameters<typeof HyperlinkMarkdown>[5];
/** Container-backed markdown message that can update text in place. */
export class MarkdownMessageComponent extends Container {
private body: HyperlinkMarkdown;
constructor(text: string, y: number, options?: MarkdownOptions) {
constructor(
text: string,
y: number,
defaultTextStyle?: DefaultTextStyle,
options?: MarkdownOptions,
) {
super();
this.body = new HyperlinkMarkdown(text, 0, y, markdownTheme, options);
this.body = new HyperlinkMarkdown(text, 0, y, markdownTheme, defaultTextStyle, options);
this.addChild(new Spacer(1));
this.addChild(this.body);
}
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { normalizeTestText } from "../../../test/helpers/normalize-text.js";
import { UserMessageComponent } from "./user-message.js";
describe("UserMessageComponent", () => {
it("preserves ordered-list markers and backslash escapes", () => {
const message = new UserMessageComponent(String.raw`7. first
9. second
Escaped \*literal\*`);
const rendered = message.render(80).map(normalizeTestText).join("\n");
expect(rendered).toContain("7. first");
expect(rendered).toContain("9. second");
expect(rendered).toContain(String.raw`\*literal\*`);
});
});
+12 -4
View File
@@ -5,9 +5,17 @@ import { MarkdownMessageComponent } from "./markdown-message.js";
/** Markdown chat-log row styled as user input. */
export class UserMessageComponent extends MarkdownMessageComponent {
constructor(text: string) {
super(text, 1, {
bgColor: (line) => theme.userBg(line),
color: (line) => theme.userText(line),
});
super(
text,
1,
{
bgColor: (line) => theme.userBg(line),
color: (line) => theme.userText(line),
},
{
preserveOrderedListMarkers: true,
preserveBackslashEscapes: true,
},
);
}
}