Merge pull request #28 from TheOneironaut/chat-bidi-rtl-support

Add BiDi support to chat text
This commit is contained in:
Federico Jaramillo Martinez
2026-06-26 00:16:13 +02:00
committed by GitHub
6 changed files with 33 additions and 10 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Improve chat, prompt, and session text rendering for RTL and mixed-direction content.
+14
View File
@@ -55,6 +55,7 @@ export class CodeViewer extends LitElement {
EditorView.editable.of(false),
EditorView.lineWrapping,
viewerTheme,
...bidiTextExtensions(this.language),
...languageExtensions(this.language),
],
}),
@@ -97,6 +98,19 @@ const viewerTheme = EditorView.theme({
},
});
const bidiTextTheme = EditorView.theme({
".cm-content": {
textAlign: "start",
},
".cm-line": {
unicodeBidi: "plaintext",
},
});
function bidiTextExtensions(language: string | undefined): Extension[] {
return language === "markdown" ? [EditorView.contentAttributes.of({ dir: "auto" }), bidiTextTheme] : [];
}
function languageExtensions(language: string | undefined): Extension[] {
if (language === undefined) return [];
switch (language) {
+1 -1
View File
@@ -9,7 +9,7 @@ export class FormattedText extends LitElement {
@property() text = "";
override render() {
return html`<div class="formatted" @click=${this.onFormattedClick}>${unsafeHTML(toSafeMarkdownHtml(this.text))}</div>`;
return html`<div class="formatted" dir="auto" @click=${this.onFormattedClick}>${unsafeHTML(toSafeMarkdownHtml(this.text))}</div>`;
}
override updated(): void {
@@ -500,6 +500,7 @@ const proseInputAssistanceAttributes: Record<string, string> = {
autocorrect: "on",
autocapitalize: "sentences",
writingsuggestions: "true",
dir: "auto",
};
const codeLikeInputAssistanceAttributes: Record<string, string> = {
@@ -507,6 +508,7 @@ const codeLikeInputAssistanceAttributes: Record<string, string> = {
autocorrect: "off",
autocapitalize: "off",
writingsuggestions: "false",
dir: "auto",
};
function inputAssistanceContentAttributes(draftBeforeCursor: string): Record<string, string> {
+3 -2
View File
@@ -134,7 +134,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
const selectedTitle = this.selected?.path ?? selectedSummary;
return html`
<h2>
<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Sessions</span>${this.collapsed ? html`<small class="section-selected" title=${selectedTitle}>${selectedSummary}</small>` : null}</span></button>
<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Sessions</span>${this.collapsed ? html`<small class="section-selected" dir="auto" title=${selectedTitle}>${selectedSummary}</small>` : null}</span></button>
${this.renderCurrentSelectionButton(currentSessions)}
<small class="section-count">${sessionCount}</small>
<button ?disabled=${!this.canStart} @click=${(event: MouseEvent) => { event.stopPropagation(); this.onStart?.(); }}>+</button>
@@ -213,7 +213,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
>
<div class="action-main ${selectionActive ? "selecting" : ""}">
${showsCheckbox ? html`<input class="session-checkbox" type="checkbox" aria-label=${`Select ${sessionLabel(session)}`} .checked=${bulkSelected} @click=${(event: MouseEvent) => { event.stopPropagation(); }} @change=${() => { this.toggleSelected(session.id); }}>` : null}
<span class="action-name">${row.depth > 0 ? html`<span class="tree-marker">↳</span>` : null}${sessionLabel(session)}${row.depth > 2 ? html` <span class="badge">depth ${row.depth}</span>` : null}${row.hasMissingParent ? html` <span class="badge">parent unavailable</span>` : null}</span><small>${this.renderSessionMetaPrefix(session)}${String(session.messageCount)} messages</small>
<span class="action-name" dir="auto">${row.depth > 0 ? html`<span class="tree-marker">↳</span>` : null}${sessionLabel(session)}${row.depth > 2 ? html` <span class="badge">depth ${row.depth}</span>` : null}${row.hasMissingParent ? html` <span class="badge">parent unavailable</span>` : null}</span><small>${this.renderSessionMetaPrefix(session)}${String(session.messageCount)} messages</small>
${this.renderActivity(session)}
</div>
<div class="action-menu">
@@ -375,6 +375,7 @@ export class SessionList extends LitElement implements KeyboardNavigableSection
.bulk-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 6px; }
.bulk-row button { padding: 5px 7px; font-size: 12px; }
.bulk-row small { display: inline; min-width: 0; color: var(--pi-muted); }
.action-name, .section-selected { text-align: start; unicode-bidi: plaintext; }
.bulk-row .capability-hint { flex: 1 0 100%; color: var(--pi-warning); }
.bulk-row.selecting { padding: 6px; border: 1px solid var(--pi-border-muted); border-radius: 8px; background: color-mix(in srgb, var(--pi-surface) 65%, transparent); }
button.danger, .action-menu-panel button.danger { color: var(--pi-danger); }
+8 -7
View File
@@ -345,6 +345,7 @@ export const chatStyles = css`
.msg-meta:focus::before, .msg-meta.expanded::before { content: ""; }
}
formatted-text.part { display: block; }
formatted-text.part, .queued-message formatted-text { text-align: start; unicode-bidi: plaintext; }
.part { max-width: 100%; min-width: 0; box-sizing: border-box; overflow: visible; }
.part + .part { margin-top: 10px; }
.tool-line { color: var(--pi-warning); }
@@ -355,22 +356,22 @@ export const chatStyles = css`
.skill-invocation > summary, .skill-read > strong { color: var(--pi-purple); }
.skill-invocation > small, .skill-read > small { display: block; margin: 6px 0 0; color: var(--pi-muted); }
summary { cursor: pointer; color: var(--pi-muted); }
pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
.shell-output { color: var(--pi-text); font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }
pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; direction: ltr; text-align: left; unicode-bidi: isolate; }
.shell-output { color: var(--pi-text); font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; direction: ltr; text-align: left; unicode-bidi: isolate; }
@keyframes pulse { 0%, 100% { transform: scale(.75); opacity: .55; } 50% { transform: scale(1.2); opacity: 1; } }
`;
export const formattedTextStyles = css`
:host { display: block; }
.formatted { white-space: normal; overflow-wrap: anywhere; line-height: 1.45; }
.formatted { white-space: normal; overflow-wrap: anywhere; line-height: 1.45; text-align: start; unicode-bidi: plaintext; }
p, ul, ol, pre, blockquote, table, .code-block-wrapper { margin: 0 0 10px; }
:is(p, ul, ol, pre, blockquote, table, .code-block-wrapper):last-child { margin-bottom: 0; }
ul, ol { padding-left: 22px; }
li + li { margin-top: 3px; }
code { border: 1px solid var(--pi-border); border-radius: 4px; background: var(--pi-bg); padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
code { border: 1px solid var(--pi-border); border-radius: 4px; background: var(--pi-bg); padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; direction: ltr; text-align: left; unicode-bidi: isolate; }
.code-block-wrapper { position: relative; }
.code-block-wrapper pre { margin: 0; padding-right: 40px; }
pre { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); padding: 10px; overflow-x: auto; overflow-y: hidden; }
pre { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); padding: 10px; overflow-x: auto; overflow-y: hidden; direction: ltr; text-align: left; unicode-bidi: isolate; }
pre code { border: 0; padding: 0; background: transparent; }
.code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
.code-copy-button:hover, .code-copy-button:focus { color: var(--pi-text); border-color: var(--pi-accent); }
@@ -466,8 +467,8 @@ export const promptEditorStyles = css`
textarea, .markdown-editor .cm-editor { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow: hidden; border-radius: 8px; border: 1px solid var(--pi-border); background: var(--pi-bg); color: var(--pi-text); font: 16px/1.4 system-ui, sans-serif; }
textarea { overflow-y: auto; padding: 8px; }
.markdown-editor .cm-scroller { max-height: 220px; overflow-y: auto; font-family: system-ui, sans-serif; line-height: 1.4; }
.markdown-editor .cm-content { min-height: 38px; padding: 8px 44px 8px 8px; caret-color: var(--pi-text); }
.markdown-editor .cm-line { padding: 0; }
.markdown-editor .cm-content { min-height: 38px; padding: 8px 44px 8px 8px; caret-color: var(--pi-text); text-align: start; unicode-bidi: plaintext; }
.markdown-editor .cm-line { padding: 0; unicode-bidi: plaintext; }
.markdown-editor .cm-placeholder { color: var(--pi-dim); }
.markdown-editor .cm-focused { outline: none; }
.shell-mode textarea, .shell-mode .markdown-editor .cm-editor { border-color: var(--pi-success); box-shadow: 0 0 0 1px var(--pi-success-ring); }