thepopebot 1.2.76-beta.7 → 1.2.76-beta.9

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/api/index.js CHANGED
@@ -3,7 +3,7 @@ import { createAgentJob } from '../lib/tools/create-agent-job.js';
3
3
  import { setWebhook } from '../lib/tools/telegram.js';
4
4
  import { getAgentJobStatus, fetchAgentJobLog } from '../lib/tools/github.js';
5
5
  import { getTelegramAdapter } from '../lib/channels/index.js';
6
- import { chat, summarizeAgentJob } from '../lib/ai/index.js';
6
+ import { chat, chatStream, summarizeAgentJob } from '../lib/ai/index.js';
7
7
  import { createNotification } from '../lib/db/notifications.js';
8
8
  import { loadTriggers } from '../lib/triggers.js';
9
9
  import { verifyApiKey } from '../lib/db/api-keys.js';
@@ -211,19 +211,33 @@ async function handleTelegramWebhook(request) {
211
211
  /**
212
212
  * Process a normalized message through the AI layer with channel UX.
213
213
  * Message persistence is handled centrally by the AI layer.
214
+ *
215
+ * Uses chatStream() for progressive tool-call rendering when the adapter
216
+ * supports it (Telegram: sends each tool call as a message, reacts on completion).
217
+ * Falls back to chat() for adapters without streamChatResponse.
214
218
  */
215
219
  async function processChannelMessage(adapter, normalized) {
216
220
  await adapter.acknowledge(normalized.metadata);
217
221
  const stopIndicator = adapter.startProcessingIndicator(normalized.metadata);
218
222
 
219
223
  try {
220
- const response = await chat(
221
- normalized.threadId,
222
- normalized.text,
223
- normalized.attachments,
224
- { userId: 'telegram', chatTitle: 'Telegram' }
225
- );
226
- await adapter.sendResponse(normalized.threadId, response, normalized.metadata);
224
+ if (adapter.streamChatResponse) {
225
+ const chunks = chatStream(
226
+ normalized.threadId,
227
+ normalized.text,
228
+ normalized.attachments,
229
+ { userId: 'telegram', chatTitle: 'Telegram' }
230
+ );
231
+ await adapter.streamChatResponse(normalized.metadata.chatId, chunks);
232
+ } else {
233
+ const response = await chat(
234
+ normalized.threadId,
235
+ normalized.text,
236
+ normalized.attachments,
237
+ { userId: 'telegram', chatTitle: 'Telegram' }
238
+ );
239
+ await adapter.sendResponse(normalized.threadId, response, normalized.metadata);
240
+ }
227
241
  } catch (err) {
228
242
  console.error('Failed to process message with AI:', err);
229
243
  await adapter
package/lib/ai/index.js CHANGED
@@ -178,19 +178,19 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
178
178
  const setupArgs = { repo, branch, featureBranch };
179
179
 
180
180
  if (needsSetup) {
181
- yield { type: 'tool-call', toolCallId: setupToolCallId, toolName: 'set_up_workspace', args: setupArgs };
181
+ yield { type: 'tool-call', toolCallId: setupToolCallId, toolName: 'workspace', args: setupArgs };
182
182
  }
183
183
 
184
184
  try {
185
- await ensureWorkspaceRepo({ workspaceDir: repoDir, repo, branch, featureBranch });
185
+ const setupOutput = await ensureWorkspaceRepo({ workspaceDir: repoDir, repo, branch, featureBranch });
186
186
  ensureSkills(repoDir, isCodeMode ? 'code' : 'agent');
187
187
  if (needsSetup) {
188
- const result = `Workspace ready on ${featureBranch || branch}`;
188
+ const result = setupOutput || `Workspace ready on ${featureBranch || branch}`;
189
189
  yield { type: 'tool-result', toolCallId: setupToolCallId, result };
190
190
  persistMessage(threadId, 'assistant', JSON.stringify({
191
191
  type: 'tool-invocation',
192
192
  toolCallId: setupToolCallId,
193
- toolName: 'set_up_workspace',
193
+ toolName: 'workspace',
194
194
  state: 'output-available',
195
195
  input: setupArgs,
196
196
  output: result,
@@ -32,20 +32,24 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
32
32
  if (ghToken) env.GH_TOKEN = ghToken;
33
33
 
34
34
  const execOpts = { cwd: workspaceDir, env };
35
+ const log = [];
35
36
 
36
37
  // 1. Create workspace directory
37
38
  mkdirSync(workspaceDir, { recursive: true });
38
39
 
39
40
  // 2. Configure git to use GH_TOKEN for GitHub HTTPS URLs (mirrors setup-git.sh)
40
41
  if (ghToken) {
41
- await run('gh', ['auth', 'setup-git'], execOpts);
42
+ const out = await run('gh', ['auth', 'setup-git'], execOpts);
43
+ if (out) log.push(out);
42
44
  }
43
45
 
44
46
  // 3. Clone if not already a git repo
45
47
  const hasGit = existsSync(path.join(workspaceDir, '.git'));
46
48
  if (!hasGit) {
47
49
  if (!repo) throw new Error('ensureWorkspaceRepo: repo is required for initial clone');
48
- await run('git', ['clone', '--branch', branch || 'main', `https://github.com/${repo}`, '.'], execOpts);
50
+ const out = await run('git', ['clone', '--branch', branch || 'main', `https://github.com/${repo}`, '.'], execOpts);
51
+ log.push(`Cloned ${repo} (branch: ${branch || 'main'})`);
52
+ if (out) log.push(out);
49
53
  }
50
54
 
51
55
  // 3. Git identity (only if not already configured)
@@ -61,6 +65,7 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
61
65
  const email = user.email || `${user.id}+${user.login}@users.noreply.github.com`;
62
66
  await run('git', ['config', 'user.name', name], execOpts);
63
67
  await run('git', ['config', 'user.email', email], execOpts);
68
+ log.push(`Git identity: ${name} <${email}>`);
64
69
  } catch (err) {
65
70
  console.error('[workspace-setup] Failed to set git identity:', err.message);
66
71
  }
@@ -68,7 +73,7 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
68
73
  }
69
74
 
70
75
  // 4. Feature branch checkout
71
- if (!featureBranch) return;
76
+ if (!featureBranch) return log.join('\n');
72
77
 
73
78
  // Already on the right branch locally?
74
79
  try {
@@ -77,8 +82,11 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
77
82
  const current = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], execOpts);
78
83
  if (current !== featureBranch) {
79
84
  await run('git', ['checkout', featureBranch], execOpts);
85
+ log.push(`Checked out ${featureBranch}`);
86
+ } else {
87
+ log.push(`Already on ${featureBranch}`);
80
88
  }
81
- return;
89
+ return log.join('\n');
82
90
  } catch {
83
91
  // Branch doesn't exist locally — check remote
84
92
  }
@@ -88,15 +96,20 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
88
96
  if (remoteCheck) {
89
97
  // Remote branch exists — checkout tracking it
90
98
  await run('git', ['checkout', '-B', featureBranch, `origin/${featureBranch}`], execOpts);
99
+ log.push(`Checked out ${featureBranch} (tracking origin)`);
91
100
  } else {
92
101
  // Create new branch and push
93
102
  await run('git', ['checkout', '-b', featureBranch], execOpts);
94
- await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
103
+ const pushOut = await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
104
+ log.push(`Created and pushed ${featureBranch}`);
105
+ if (pushOut) log.push(pushOut);
95
106
  }
96
107
  } catch (err) {
97
108
  console.error('[workspace-setup] Feature branch error:', err.message);
98
109
  throw err;
99
110
  }
111
+
112
+ return log.join('\n');
100
113
  }
101
114
 
102
115
  /**
@@ -4,8 +4,10 @@ import {
4
4
  downloadFile,
5
5
  reactToMessage,
6
6
  startTypingIndicator,
7
+ formatToolCall,
7
8
  } from '../tools/telegram.js';
8
9
  import { isWhisperEnabled, transcribeAudio } from '../tools/openai.js';
10
+ import { getConfig } from '../config.js';
9
11
 
10
12
  class TelegramAdapter extends ChannelAdapter {
11
13
  constructor(botToken) {
@@ -19,15 +21,18 @@ class TelegramAdapter extends ChannelAdapter {
19
21
  * Returns null if the update should be ignored.
20
22
  */
21
23
  async receive(request) {
22
- const { TELEGRAM_WEBHOOK_SECRET, TELEGRAM_CHAT_ID, TELEGRAM_VERIFICATION } = process.env;
24
+ // Read config from DB (with .env fallback via getConfig)
25
+ const webhookSecret = getConfig('TELEGRAM_WEBHOOK_SECRET');
26
+ const allowedChatId = getConfig('TELEGRAM_CHAT_ID');
27
+ const verificationCode = getConfig('TELEGRAM_VERIFICATION');
23
28
 
24
29
  // Validate secret token (required)
25
- if (!TELEGRAM_WEBHOOK_SECRET) {
30
+ if (!webhookSecret) {
26
31
  console.error('[telegram] TELEGRAM_WEBHOOK_SECRET not configured — rejecting webhook');
27
32
  return null;
28
33
  }
29
34
  const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
30
- if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
35
+ if (headerSecret !== webhookSecret) {
31
36
  return null;
32
37
  }
33
38
 
@@ -40,17 +45,27 @@ class TelegramAdapter extends ChannelAdapter {
40
45
  let text = message.text || null;
41
46
  const attachments = [];
42
47
 
43
- // Check for verification code — works even before TELEGRAM_CHAT_ID is set
44
- if (TELEGRAM_VERIFICATION && text === TELEGRAM_VERIFICATION) {
48
+ // Check for verification code — works even before TELEGRAM_CHAT_ID is set.
49
+ // On match, save the chat ID directly and clear the (one-time) verification code.
50
+ if (verificationCode && text === verificationCode) {
45
51
  await sendMessage(this.botToken, chatId, `Your chat ID:\n<code>${chatId}</code>`);
52
+ try {
53
+ const { setConfigValue, deleteConfigValue } = await import('../db/config.js');
54
+ const { invalidateConfigCache } = await import('../config.js');
55
+ setConfigValue('TELEGRAM_CHAT_ID', chatId);
56
+ deleteConfigValue('TELEGRAM_VERIFICATION');
57
+ invalidateConfigCache();
58
+ } catch (err) {
59
+ console.error('[telegram] Failed to persist verified chat ID:', err.message);
60
+ }
46
61
  return null;
47
62
  }
48
63
 
49
64
  // Security: if no TELEGRAM_CHAT_ID configured, ignore all messages
50
- if (!TELEGRAM_CHAT_ID) return null;
65
+ if (!allowedChatId) return null;
51
66
 
52
67
  // Security: only accept messages from configured chat
53
- if (chatId !== TELEGRAM_CHAT_ID) return null;
68
+ if (chatId !== allowedChatId) return null;
54
69
 
55
70
  // Voice messages → transcribe to text
56
71
  if (message.voice) {
@@ -140,6 +155,62 @@ class TelegramAdapter extends ChannelAdapter {
140
155
  await sendMessage(this.botToken, threadId, text);
141
156
  }
142
157
 
158
+ /**
159
+ * Consume a chatStream() async iterable and send progressive messages.
160
+ * - Text chunks accumulate and flush when a tool-call arrives or stream ends.
161
+ * - Each tool-call sends immediately as its own message.
162
+ * - Tool-results react to the tool-call message with ✅ (or ❌ on error).
163
+ *
164
+ * @param {string} chatId - Telegram chat ID
165
+ * @param {AsyncIterable} chunks - chatStream() output
166
+ */
167
+ async streamChatResponse(chatId, chunks) {
168
+ let textBuffer = '';
169
+ // Map toolCallId → { telegramMessageId, hasCompleteArgs }
170
+ const toolMessages = new Map();
171
+
172
+ for await (const chunk of chunks) {
173
+ if (chunk.type === 'text') {
174
+ textBuffer += chunk.text;
175
+ } else if (chunk.type === 'tool-call') {
176
+ // Skip the first empty-args emission — wait for complete args
177
+ if (!chunk.args || Object.keys(chunk.args).length === 0) {
178
+ continue;
179
+ }
180
+
181
+ // Flush accumulated text before tool call
182
+ if (textBuffer.trim()) {
183
+ await sendMessage(this.botToken, chatId, textBuffer.trim());
184
+ textBuffer = '';
185
+ }
186
+
187
+ // Send tool call as its own message
188
+ const text = formatToolCall(chunk.toolName, chunk.args);
189
+ try {
190
+ const msg = await sendMessage(this.botToken, chatId, text);
191
+ toolMessages.set(chunk.toolCallId, msg.message_id);
192
+ } catch (err) {
193
+ console.error('[telegram] Failed to send tool call:', err.message);
194
+ }
195
+ } else if (chunk.type === 'tool-result') {
196
+ const messageId = toolMessages.get(chunk.toolCallId);
197
+ if (messageId) {
198
+ const emoji = chunk.result?.includes?.('error') || chunk.result?.includes?.('Error')
199
+ ? '👎'
200
+ : '👍';
201
+ reactToMessage(this.botToken, chatId, messageId, emoji).catch(() => {});
202
+ toolMessages.delete(chunk.toolCallId);
203
+ }
204
+ }
205
+ // Skip: meta, result, thinking-*, unknown
206
+ }
207
+
208
+ // Flush remaining text
209
+ if (textBuffer.trim()) {
210
+ await sendMessage(this.botToken, chatId, textBuffer.trim());
211
+ }
212
+ }
213
+
143
214
  get supportsStreaming() {
144
215
  return false;
145
216
  }
@@ -966,6 +966,151 @@ export async function regenerateWebhookSecret(key) {
966
966
  }
967
967
  }
968
968
 
969
+ // ─────────────────────────────────────────────────────────────────────────────
970
+ // Settings — Telegram setup flow
971
+ // ─────────────────────────────────────────────────────────────────────────────
972
+
973
+ /**
974
+ * Get Telegram status: bot info (if token valid), webhook info, chat ID.
975
+ * Used by the admin UI to render the current state of each setup step.
976
+ */
977
+ export async function getTelegramStatus() {
978
+ await requireAuth();
979
+ try {
980
+ const { getConfigSecret, getConfigValue } = await import('../db/config.js');
981
+ const { validateBotToken, getTelegramWebhookInfo } = await import('../tools/telegram.js');
982
+
983
+ const botToken = getConfigSecret('TELEGRAM_BOT_TOKEN');
984
+ const webhookSecret = getConfigSecret('TELEGRAM_WEBHOOK_SECRET');
985
+ const chatId = getConfigValue('TELEGRAM_CHAT_ID');
986
+ const verificationCode = getConfigValue('TELEGRAM_VERIFICATION');
987
+
988
+ let botInfo = null;
989
+ let webhookInfo = null;
990
+ if (botToken) {
991
+ const v = await validateBotToken(botToken);
992
+ if (v.valid) botInfo = { username: v.botInfo.username, id: v.botInfo.id };
993
+ try {
994
+ const info = await getTelegramWebhookInfo(botToken);
995
+ if (info.ok) {
996
+ webhookInfo = {
997
+ url: info.result.url || null,
998
+ hasCustomCertificate: info.result.has_custom_certificate,
999
+ pendingUpdates: info.result.pending_update_count,
1000
+ lastErrorMessage: info.result.last_error_message || null,
1001
+ };
1002
+ }
1003
+ } catch {
1004
+ // ignore — webhook info is best-effort
1005
+ }
1006
+ }
1007
+
1008
+ return {
1009
+ botInfo,
1010
+ botTokenSet: !!botToken,
1011
+ webhookSecretSet: !!webhookSecret,
1012
+ webhookInfo,
1013
+ chatId: chatId || null,
1014
+ pendingVerification: !!verificationCode,
1015
+ };
1016
+ } catch (err) {
1017
+ console.error('Failed to get Telegram status:', err);
1018
+ return { error: 'Failed to load Telegram status' };
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * Validate a Telegram bot token without saving it.
1024
+ * Called from the UI as the user pastes a token to show live feedback.
1025
+ */
1026
+ export async function validateTelegramToken(token) {
1027
+ await requireAuth();
1028
+ if (!token) return { valid: false, error: 'Token is required' };
1029
+ try {
1030
+ const { validateBotToken } = await import('../tools/telegram.js');
1031
+ const result = await validateBotToken(token);
1032
+ if (result.valid) {
1033
+ return { valid: true, botInfo: { username: result.botInfo.username, id: result.botInfo.id } };
1034
+ }
1035
+ return { valid: false, error: result.error };
1036
+ } catch (err) {
1037
+ return { valid: false, error: err.message };
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * Register the Telegram webhook with the currently saved bot token.
1043
+ * Generates a fresh webhook secret, saves it, and calls Telegram's setWebhook.
1044
+ * APP_URL must be configured.
1045
+ */
1046
+ export async function registerTelegramWebhook() {
1047
+ const user = await requireAuth();
1048
+ try {
1049
+ const { getConfigSecret, setConfigSecret } = await import('../db/config.js');
1050
+ const { invalidateConfigCache, getConfig } = await import('../config.js');
1051
+ const { setTelegramWebhook, generateWebhookSecret } = await import('../tools/telegram.js');
1052
+
1053
+ const botToken = getConfigSecret('TELEGRAM_BOT_TOKEN');
1054
+ if (!botToken) return { error: 'Bot token must be set first' };
1055
+
1056
+ const appUrl = getConfig('APP_URL');
1057
+ if (!appUrl) return { error: 'APP_URL must be configured first' };
1058
+
1059
+ const webhookUrl = `${appUrl.replace(/\/$/, '')}/api/telegram/webhook`;
1060
+ const secret = generateWebhookSecret();
1061
+ setConfigSecret('TELEGRAM_WEBHOOK_SECRET', secret, user.id);
1062
+ invalidateConfigCache();
1063
+
1064
+ const result = await setTelegramWebhook(botToken, webhookUrl, secret);
1065
+ if (!result.ok) {
1066
+ return { error: result.description || 'Failed to register webhook' };
1067
+ }
1068
+ return { success: true, webhookUrl };
1069
+ } catch (err) {
1070
+ console.error('Failed to register Telegram webhook:', err);
1071
+ return { error: err.message };
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Start verification: generate a code, save it, return it to the UI.
1077
+ * User sends the code to the bot; the webhook handler captures the chat ID,
1078
+ * saves it as TELEGRAM_CHAT_ID, and clears this verification code.
1079
+ * The UI polls getTelegramStatus() to detect completion.
1080
+ */
1081
+ export async function startTelegramVerification() {
1082
+ const user = await requireAuth();
1083
+ try {
1084
+ const { setConfigValue } = await import('../db/config.js');
1085
+ const { invalidateConfigCache } = await import('../config.js');
1086
+ const { generateVerificationCode } = await import('../tools/telegram.js');
1087
+
1088
+ const code = generateVerificationCode();
1089
+ setConfigValue('TELEGRAM_VERIFICATION', code, user.id);
1090
+ invalidateConfigCache();
1091
+ return { success: true, code };
1092
+ } catch (err) {
1093
+ console.error('Failed to start Telegram verification:', err);
1094
+ return { error: err.message };
1095
+ }
1096
+ }
1097
+
1098
+ /**
1099
+ * Cancel pending verification (clear the code).
1100
+ */
1101
+ export async function cancelTelegramVerification() {
1102
+ await requireAuth();
1103
+ try {
1104
+ const { deleteConfigValue } = await import('../db/config.js');
1105
+ const { invalidateConfigCache } = await import('../config.js');
1106
+ deleteConfigValue('TELEGRAM_VERIFICATION');
1107
+ invalidateConfigCache();
1108
+ return { success: true };
1109
+ } catch (err) {
1110
+ return { error: err.message };
1111
+ }
1112
+ }
1113
+
969
1114
  // ─────────────────────────────────────────────────────────────────────────────
970
1115
  // Settings — Chat sub-tab
971
1116
  // ─────────────────────────────────────────────────────────────────────────────
@@ -200,7 +200,7 @@ function WorkspaceBar({
200
200
  repoName && /* @__PURE__ */ jsx("span", { className: "shrink-0 cursor-default hidden md:inline", title: repo, children: repoName }),
201
201
  branch && /* @__PURE__ */ jsxs(Fragment, { children: [
202
202
  /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground/30 hidden md:inline", children: "/" }),
203
- /* @__PURE__ */ jsx("div", { className: "shrink-0 max-w-[120px]", children: /* @__PURE__ */ jsx(
203
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 max-w-[120px] md:max-w-[160px]", children: /* @__PURE__ */ jsx(
204
204
  Combobox,
205
205
  {
206
206
  options: branches.map((b) => ({ value: b.name, label: b.name })),
@@ -218,8 +218,8 @@ function WorkspaceBar({
218
218
  }).finally(() => setLoadingBranches(false));
219
219
  }
220
220
  },
221
- triggerClassName: "font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1",
222
- triggerLabel: /* @__PURE__ */ jsx("span", { className: "truncate", title: branch, children: branch })
221
+ triggerClassName: "block w-full text-left font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1",
222
+ triggerLabel: /* @__PURE__ */ jsx("span", { className: "block truncate", title: branch, children: branch })
223
223
  }
224
224
  ) })
225
225
  ] }),
@@ -225,7 +225,7 @@ export function WorkspaceBar({
225
225
  {branch && (
226
226
  <>
227
227
  <span className="shrink-0 text-muted-foreground/30 hidden md:inline">/</span>
228
- <div className="shrink-0 max-w-[120px]">
228
+ <div className="min-w-0 max-w-[120px] md:max-w-[160px]">
229
229
  <Combobox
230
230
  options={branches.map((b) => ({ value: b.name, label: b.name }))}
231
231
  value={branch}
@@ -242,8 +242,8 @@ export function WorkspaceBar({
242
242
  }).finally(() => setLoadingBranches(false));
243
243
  }
244
244
  }}
245
- triggerClassName="font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1"
246
- triggerLabel={<span className="truncate" title={branch}>{branch}</span>}
245
+ triggerClassName="block w-full text-left font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1"
246
+ triggerLabel={<span className="block truncate" title={branch}>{branch}</span>}
247
247
  />
248
248
  </div>
249
249
  </>
@@ -434,7 +434,7 @@ function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
434
434
  function ThinkingMessage() {
435
435
  return /* @__PURE__ */ jsx("div", { className: "flex gap-4 w-full justify-start", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground", children: [
436
436
  /* @__PURE__ */ jsx(SpinnerIcon, { size: 14 }),
437
- /* @__PURE__ */ jsx("span", { children: "Thinking..." })
437
+ /* @__PURE__ */ jsx("span", { className: "thinking-shimmer", children: "Waiting..." })
438
438
  ] }) });
439
439
  }
440
440
  export {
@@ -523,7 +523,7 @@ export function ThinkingMessage() {
523
523
  <div className="flex gap-4 w-full justify-start">
524
524
  <div className="flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground">
525
525
  <SpinnerIcon size={14} />
526
- <span>Thinking...</span>
526
+ <span className="thinking-shimmer">Waiting...</span>
527
527
  </div>
528
528
  </div>
529
529
  );