Render icon markers in composer progress

Replace the textarea-only progress placeholder with a safe overlay so icon:// markers render as Material Symbols while preserving the ghost-text behavior. Escape surrounding text before injecting rendered marker HTML.

Alessandro committed May 18, 2026 at 02:21 UTC 82280950ea9e87e7f20481a39137ce3b4413d08b
2 files changed +94 -4
webui/components/chat/input/chat-bar-input.html
+38
@@ -44,6 +44,14 @@
44 @keydown.down="$store.chatInput.historyNext($event)"
45 @input="$store.chatInput.adjustTextareaHeight()"
46 x-model="$store.chatInput.message"></textarea>
47 + <div
48 + id="chat-input-progress-placeholder"
49 + x-show="$store.chatInput.showProgressPlaceholder"
50 + x-html="$store.chatInput.progressPlaceholderHtml"
51 + :class="{ 'progress-active': $store.chatInput.progressActive }"
52 + aria-hidden="true"
53 + style="display: none;"
54 + ></div>
55 <button id="expand-button" @click="$store.fullScreenInputModal.openModal()" aria-label="Expand input">
56 <span class="material-symbols-outlined" aria-hidden="true">open_in_full</span>
57 </button>
@@ -172,6 +180,36 @@
180 border-color: color-mix(in srgb, var(--color-highlight) 20%, transparent);
181 outline-color: color-mix(in srgb, var(--color-highlight) 15%, transparent);
182 }
183 + #chat-input-progress-placeholder {
184 + position: absolute;
185 + top: calc(6px + 0.48rem);
186 + left: calc(6px + var(--spacing-sm));
187 + right: 46px;
188 + color: var(--color-text-muted);
189 + opacity: 0.7;
190 + font-family: "Roboto Mono", monospace;
191 + font-size: 0.9rem;
192 + line-height: 1.45;
193 + white-space: nowrap;
194 + overflow: hidden;
195 + text-overflow: ellipsis;
196 + pointer-events: none;
197 + display: flex;
198 + align-items: center;
199 + gap: 0.35rem;
200 + }
201 + #chat-input-progress-placeholder.progress-active {
202 + animation: placeholder-pulse 2s ease-in-out infinite;
203 + }
204 + #chat-input-progress-placeholder .chat-input-progress-cue {
205 + color: var(--color-text-muted);
206 + }
207 + #chat-input-progress-placeholder .chat-input-progress-icon {
208 + font-size: 1.15rem;
209 + line-height: 1;
210 + color: var(--color-highlight);
211 + vertical-align: -0.18rem;
212 + }
213
214 #expand-button {
215 position: absolute;
webui/components/chat/input/input-store.js
+56 -4
@@ -5,6 +5,46 @@ import { store as messageQueueStore } from "/components/chat/message-queue/messa
5 import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
6 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
7
8 +const ICON_MARKER_RE = /icon:\/\/([a-zA-Z0-9_]+)(\[(?:\\.|[^\]])*\])?/g;
9 +
10 +function escapeHTML(value) {
11 + return String(value ?? "")
12 + .replace(/&/g, "&amp;")
13 + .replace(/</g, "&lt;")
14 + .replace(/>/g, "&gt;")
15 + .replace(/"/g, "&quot;")
16 + .replace(/'/g, "&#039;");
17 +}
18 +
19 +function unescapeIconTooltip(block) {
20 + if (!block) return "";
21 + return block
22 + .slice(1, -1)
23 + .replace(/\\\[/g, "[")
24 + .replace(/\\\]/g, "]")
25 + .replace(/\\\\/g, "\\");
26 +}
27 +
28 +function convertIconMarkersToHtml(value) {
29 + const text = String(value ?? "");
30 + let html = "";
31 + let lastIndex = 0;
32 +
33 + text.replace(ICON_MARKER_RE, (match, iconName, tooltipBlock, offset) => {
34 + html += escapeHTML(text.slice(lastIndex, offset));
35 + const tooltip = unescapeIconTooltip(tooltipBlock) || iconName;
36 + html += (
37 + `<span class="icon material-symbols-outlined chat-input-progress-icon" ` +
38 + `title="${escapeHTML(tooltip)}">${escapeHTML(iconName)}</span>`
39 + );
40 + lastIndex = offset + match.length;
41 + return match;
42 + });
43 +
44 + html += escapeHTML(text.slice(lastIndex));
45 + return html;
46 +}
47 +
48 const model = {
49 paused: false,
50 message: "",
@@ -38,13 +78,25 @@ const model = {
78 get inputPlaceholder() {
79 const state = this._getSendState();
80 if (state === "all") return "Press Enter to send queued messages";
41 - // Show progress as ghost text when agent is working and input is empty
42 - if (this.progressText && !this.message) {
43 - return "|> " + this.progressText;
44 - }
81 + if (this.showProgressPlaceholder) return "";
82 return "Type your message here...";
83 },
84
85 + get showProgressPlaceholder() {
86 + return (
87 + this._getSendState() !== "all" &&
88 + !!this.progressText &&
89 + !this.message
90 + );
91 + },
92 +
93 + get progressPlaceholderHtml() {
94 + return (
95 + `<span class="chat-input-progress-cue">|&gt;</span> ` +
96 + convertIconMarkersToHtml(this.progressText)
97 + );
98 + },
99 +
100 // Computed: send button icon type
101 get sendButtonIcon() {
102 const state = this._getSendState();