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, "&")
13
+ .replace(/</g, "<")
14
+ .replace(/>/g, ">")
15
+ .replace(/"/g, """)
16
+ .replace(/'/g, "'");
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">|></span> ` +
96
+ convertIconMarkersToHtml(this.progressText)
97
+ );
98
+ },
99
+
100
// Computed: send button icon type
101
get sendButtonIcon() {
102
const state = this._getSendState();