@astra-code/astra-ai 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  `astra-code-term` is the npm-first Astra terminal client built with TypeScript, React, and Ink.
4
4
 
5
+ ## Repository structure
6
+
7
+ - `.env.example` — sample configuration
8
+ - `docs/` — supplemental documentation
9
+ - `install.sh` — helper script for setup
10
+ - `src/` — Ink CLI source code (entrypoint in `src/index.ts`)
11
+ - `tsconfig.json` — shared TypeScript configuration
12
+
5
13
  ## Install
6
14
 
7
15
  ### Global (recommended)
package/dist/app/App.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { Box, Text, useApp, useInput } from "ink";
4
4
  import Spinner from "ink-spinner";
@@ -32,6 +32,7 @@ const ASTRA_ASCII = `
32
32
  ### ### ######## ### ### ### ### ### ######## ######## ######### ##########
33
33
  by Sean Donovan
34
34
  `;
35
+ const ASTRA_COMPACT = "ASTRA CODE";
35
36
  const WELCOME_WIDTH = 96;
36
37
  const centerLine = (text, width = WELCOME_WIDTH) => {
37
38
  const trimmed = text.trim();
@@ -44,6 +45,9 @@ const centerLine = (text, width = WELCOME_WIDTH) => {
44
45
  const FOUNDER_WELCOME = centerLine("Welcome to Astra from Astra CEO & Founder, Sean Donovan");
45
46
  const HISTORY_SETTINGS_URL = "https://astra-web-builder.vercel.app/settings";
46
47
  const VOICE_SILENCE_MS = Number(process.env.ASTRA_VOICE_SILENCE_MS ?? "3000");
48
+ const VOICE_MIN_CHARS = Number(process.env.ASTRA_VOICE_MIN_CHARS ?? "4");
49
+ const VOICE_DUPLICATE_WINDOW_MS = Number(process.env.ASTRA_VOICE_DUPLICATE_WINDOW_MS ?? "10000");
50
+ const VOICE_NOISE_WORDS = new Set(["you", "yes", "yeah", "yep", "ok", "okay", "uh", "um", "hmm"]);
47
51
  const TOOL_SNIPPET_LINES = 6;
48
52
  const NOISY_EVENT_TYPES = new Set([
49
53
  "timing",
@@ -145,8 +149,10 @@ const extractAssistantText = (event) => {
145
149
  }
146
150
  return null;
147
151
  };
148
- const DIVIDER = "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────";
149
152
  const LABEL_WIDTH = 10;
153
+ const HEADER_PATH_MAX = Number(process.env.ASTRA_HEADER_PATH_MAX ?? "42");
154
+ const MIN_DIVIDER = 64;
155
+ const MAX_DIVIDER = 120;
150
156
  const styleForKind = (kind) => {
151
157
  switch (kind) {
152
158
  case "assistant":
@@ -183,14 +189,83 @@ const normalizeAssistantText = (input) => {
183
189
  if (!input) {
184
190
  return "";
185
191
  }
186
- return input
192
+ let out = input
187
193
  // Remove control chars but preserve newlines/tabs.
188
194
  .replace(/[^\x09\x0A\x0D\x20-\x7E]/g, "")
195
+ .replace(/([a-z0-9/])([A-Z])/g, "$1 $2")
196
+ .replace(/([.!?])([A-Za-z])/g, "$1 $2")
197
+ .replace(/([a-z])(\u2022)/g, "$1\n$2")
198
+ .replace(/([^\n])(Summary:|Next:|Tests:)/g, "$1\n\n$2")
199
+ .replace(/([A-Za-z0-9_/-]+)\.\s+md\b/g, "$1.md")
200
+ .replace(/(\bnpm audit)(All tasks complete\.)/gi, "$1\n\n$2")
201
+ .replace(/([.!?])\s*(Summary:|Next:|Tests:)/g, "$1\n\n$2")
202
+ .replace(/([^\n])(\u2022\s)/g, "$1\n$2")
189
203
  // Trim trailing spaces line-by-line.
190
204
  .replace(/[ \t]+$/gm, "")
191
205
  // Normalize excessive blank lines.
192
- .replace(/\n{3,}/g, "\n\n")
193
- .trim();
206
+ .replace(/\n{3,}/g, "\n\n");
207
+ const paragraphs = out
208
+ .split(/\n{2,}/)
209
+ .map((p) => p.trim())
210
+ .filter(Boolean);
211
+ const seen = new Set();
212
+ const deduped = [];
213
+ for (const para of paragraphs) {
214
+ const key = para.toLowerCase().replace(/\s+/g, " ");
215
+ if (para.length > 50 && seen.has(key)) {
216
+ continue;
217
+ }
218
+ seen.add(key);
219
+ deduped.push(para);
220
+ }
221
+ return deduped.join("\n\n").trim();
222
+ };
223
+ const summarizeToolResult = (toolName, data, success) => {
224
+ if (!success) {
225
+ return `${toolName} failed`;
226
+ }
227
+ const path = typeof data.path === "string" ? data.path : "";
228
+ const totalLines = typeof data.total_lines === "number" ? data.total_lines : null;
229
+ if (toolName === "view_file") {
230
+ if (totalLines !== null && path) {
231
+ return `Read ${totalLines} lines of <${path}>`;
232
+ }
233
+ if (path) {
234
+ return `Read <${path}>`;
235
+ }
236
+ }
237
+ if (toolName === "list_directory") {
238
+ const dir = path || ".";
239
+ if (totalLines !== null) {
240
+ return `Listed ${totalLines} entries in <${dir}>`;
241
+ }
242
+ return `Listed <${dir}>`;
243
+ }
244
+ if (toolName === "semantic_search") {
245
+ return "Searched codebase context";
246
+ }
247
+ if (toolName === "search_files") {
248
+ return totalLines !== null ? `Found ${totalLines} matching paths` : "Searched file paths";
249
+ }
250
+ return `${toolName} completed`;
251
+ };
252
+ const isLikelyVoiceNoise = (text) => {
253
+ const normalized = text.trim().toLowerCase();
254
+ if (!normalized) {
255
+ return true;
256
+ }
257
+ if (normalized.length < VOICE_MIN_CHARS) {
258
+ return true;
259
+ }
260
+ const tokens = normalized.split(/\s+/).filter(Boolean);
261
+ if (tokens.length === 0) {
262
+ return true;
263
+ }
264
+ const nonNoise = tokens.filter((t) => !VOICE_NOISE_WORDS.has(t));
265
+ if (nonNoise.length === 0) {
266
+ return true;
267
+ }
268
+ return false;
194
269
  };
195
270
  const parseInline = (line) => {
196
271
  const tokens = [];
@@ -351,6 +426,20 @@ const renderMarkdownContent = (text, baseColor, keyPrefix) => {
351
426
  };
352
427
  export const AstraApp = () => {
353
428
  const workspaceRoot = useMemo(() => process.cwd(), []);
429
+ const [terminalWidth, setTerminalWidth] = useState(() => process.stdout.columns || MAX_DIVIDER);
430
+ useEffect(() => {
431
+ const updateWidth = () => {
432
+ setTerminalWidth(process.stdout.columns || MAX_DIVIDER);
433
+ };
434
+ process.stdout.on("resize", updateWidth);
435
+ return () => {
436
+ process.stdout.off("resize", updateWidth);
437
+ };
438
+ }, []);
439
+ const dividerWidth = Math.max(MIN_DIVIDER, Math.min(MAX_DIVIDER, (terminalWidth ?? MAX_DIVIDER) - 2));
440
+ const divider = "─".repeat(dividerWidth);
441
+ const brand = dividerWidth < 96 ? ASTRA_COMPACT : ASTRA_ASCII;
442
+ const welcomeLine = dividerWidth < 96 ? "Welcome to Astra" : FOUNDER_WELCOME;
354
443
  const backend = useMemo(() => new BackendClient(), []);
355
444
  const { exit } = useApp();
356
445
  // In-session file cache: tracks files created/edited so subsequent requests
@@ -400,8 +489,10 @@ export const AstraApp = () => {
400
489
  const [streamingText, setStreamingText] = useState("");
401
490
  const [voiceEnabled, setVoiceEnabled] = useState(false);
402
491
  const [voiceListening, setVoiceListening] = useState(false);
492
+ const [voicePreparing, setVoicePreparing] = useState(false);
403
493
  const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
404
494
  const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState(null);
495
+ const [toolFeedMode, setToolFeedMode] = useState("compact");
405
496
  const [historyOpen, setHistoryOpen] = useState(false);
406
497
  const [historyMode, setHistoryMode] = useState("picker");
407
498
  const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
@@ -411,13 +502,34 @@ export const AstraApp = () => {
411
502
  const [historyIndex, setHistoryIndex] = useState(0);
412
503
  const liveVoiceRef = useRef(null);
413
504
  const voiceSilenceTimerRef = useRef(null);
505
+ const lastVoicePromptRef = useRef(null);
506
+ const lastIgnoredVoiceRef = useRef(null);
414
507
  const fileEditBuffersRef = useRef(new Map());
415
508
  const isSuperAdmin = user?.role === "super_admin";
416
509
  const pushMessage = useCallback((kind, text) => {
417
510
  setMessages((prev) => [...prev, { kind, text }].slice(-300));
418
511
  }, []);
419
512
  const pushToolCard = useCallback((card) => {
420
- setMessages((prev) => [...prev, { kind: "tool", text: card.summary, card }].slice(-300));
513
+ setMessages((prev) => {
514
+ const nextEntry = { kind: "tool", text: card.summary, card };
515
+ const last = prev[prev.length - 1];
516
+ if (last &&
517
+ last.kind === "tool" &&
518
+ last.card &&
519
+ last.card.toolName === card.toolName &&
520
+ last.card.kind === card.kind &&
521
+ last.card.summary === card.summary &&
522
+ last.card.locality === card.locality) {
523
+ const updated = [...prev];
524
+ const existingCount = Math.max(1, Number(last.card.count ?? 1));
525
+ updated[updated.length - 1] = {
526
+ ...last,
527
+ card: { ...last.card, count: existingCount + 1 }
528
+ };
529
+ return updated.slice(-300);
530
+ }
531
+ return [...prev, nextEntry].slice(-300);
532
+ });
421
533
  }, []);
422
534
  const filteredHistory = useMemo(() => {
423
535
  const q = historyQuery.trim().toLowerCase();
@@ -488,11 +600,13 @@ export const AstraApp = () => {
488
600
  }
489
601
  const controller = liveVoiceRef.current;
490
602
  if (!controller) {
603
+ setVoicePreparing(false);
491
604
  setVoiceWaitingForSilence(false);
492
605
  return;
493
606
  }
494
607
  liveVoiceRef.current = null;
495
608
  await controller.stop();
609
+ setVoicePreparing(false);
496
610
  setVoiceListening(false);
497
611
  setVoiceWaitingForSilence(false);
498
612
  }, []);
@@ -500,24 +614,45 @@ export const AstraApp = () => {
500
614
  if (liveVoiceRef.current) {
501
615
  return;
502
616
  }
617
+ // #region agent log
618
+ fetch('http://127.0.0.1:7573/ingest/fdd4f018-1ba3-4303-b1bb-375443267476', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '17f1ea' }, body: JSON.stringify({ sessionId: '17f1ea', runId: 'voice_run_1', hypothesisId: 'H5', location: 'App.tsx:startLiveVoice', message: 'startLiveVoice called', data: { announce, voiceEnabled, thinking, hasController: Boolean(liveVoiceRef.current) }, timestamp: Date.now() }) }).catch(() => { });
619
+ // #endregion
503
620
  setVoiceEnabled(true);
504
- setVoiceListening(true);
621
+ setVoicePreparing(true);
622
+ setVoiceListening(false);
505
623
  setVoiceWaitingForSilence(false);
506
624
  if (announce) {
507
- pushMessage("system", "Voice input started. Speak now…");
625
+ pushMessage("system", "Voice input armed. Preparing microphone...");
508
626
  }
509
627
  liveVoiceRef.current = startLiveTranscription({
510
628
  onPartial: (text) => {
511
- setPrompt(text);
629
+ setVoicePreparing(false);
630
+ setVoiceListening(true);
512
631
  if (voiceSilenceTimerRef.current) {
513
632
  clearTimeout(voiceSilenceTimerRef.current);
514
633
  }
515
634
  const candidate = text.trim();
516
635
  if (!candidate) {
636
+ setPrompt("");
637
+ setVoiceWaitingForSilence(false);
517
638
  return;
518
639
  }
640
+ const normalized = candidate.toLowerCase();
641
+ const isLikelyNoise = isLikelyVoiceNoise(normalized);
642
+ // #region agent log
643
+ fetch('http://127.0.0.1:7573/ingest/fdd4f018-1ba3-4303-b1bb-375443267476', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '17f1ea' }, body: JSON.stringify({ sessionId: '17f1ea', runId: 'voice_run_1', hypothesisId: 'H1', location: 'App.tsx:startLiveVoice.onPartial', message: 'partial transcript observed', data: { textLen: text.length, candidateLen: candidate.length, normalized, isLikelyNoise, silenceMs: VOICE_SILENCE_MS }, timestamp: Date.now() }) }).catch(() => { });
644
+ // #endregion
645
+ if (isLikelyNoise) {
646
+ setPrompt("");
647
+ setVoiceWaitingForSilence(false);
648
+ return;
649
+ }
650
+ setPrompt(text);
519
651
  setVoiceWaitingForSilence(true);
520
652
  voiceSilenceTimerRef.current = setTimeout(() => {
653
+ // #region agent log
654
+ fetch('http://127.0.0.1:7573/ingest/fdd4f018-1ba3-4303-b1bb-375443267476', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '17f1ea' }, body: JSON.stringify({ sessionId: '17f1ea', runId: 'voice_run_1', hypothesisId: 'H2', location: 'App.tsx:startLiveVoice.silenceTimeout', message: 'silence timeout fired and queueing prompt', data: { candidate, voiceWaitingForSilence: true }, timestamp: Date.now() }) }).catch(() => { });
655
+ // #endregion
521
656
  setVoiceQueuedPrompt(candidate);
522
657
  void stopLiveVoice();
523
658
  }, VOICE_SILENCE_MS);
@@ -529,12 +664,21 @@ export const AstraApp = () => {
529
664
  }
530
665
  setPrompt(text);
531
666
  liveVoiceRef.current = null;
667
+ setVoicePreparing(false);
532
668
  setVoiceListening(false);
533
669
  setVoiceWaitingForSilence(false);
670
+ // #region agent log
671
+ fetch('http://127.0.0.1:7573/ingest/fdd4f018-1ba3-4303-b1bb-375443267476', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '17f1ea' }, body: JSON.stringify({ sessionId: '17f1ea', runId: 'voice_run_1', hypothesisId: 'H4', location: 'App.tsx:startLiveVoice.onFinal', message: 'final transcript emitted', data: { finalLen: text.length, finalText: text.slice(0, 80) }, timestamp: Date.now() }) }).catch(() => { });
672
+ // #endregion
534
673
  },
535
674
  onError: (error) => {
675
+ setVoicePreparing(false);
676
+ setVoiceListening(false);
536
677
  setVoiceWaitingForSilence(false);
537
678
  pushMessage("error", `Voice transcription error: ${error.message}`);
679
+ // #region agent log
680
+ fetch('http://127.0.0.1:7573/ingest/fdd4f018-1ba3-4303-b1bb-375443267476', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '17f1ea' }, body: JSON.stringify({ sessionId: '17f1ea', runId: 'voice_run_1', hypothesisId: 'H4', location: 'App.tsx:startLiveVoice.onError', message: 'voice transcription error', data: { error: error.message }, timestamp: Date.now() }) }).catch(() => { });
681
+ // #endregion
538
682
  }
539
683
  });
540
684
  }, [pushMessage, stopLiveVoice]);
@@ -995,19 +1139,26 @@ export const AstraApp = () => {
995
1139
  }
996
1140
  if (event.type === "credits_exhausted") {
997
1141
  setCreditsRemaining(0);
1142
+ if (voiceEnabled) {
1143
+ await stopLiveVoice();
1144
+ setVoiceEnabled(false);
1145
+ pushMessage("system", "Voice input paused: credits exhausted. Recharge, then run /voice on.");
1146
+ pushMessage("system", "");
1147
+ }
1148
+ }
1149
+ if (event.type === "continuation_check") {
1150
+ const recommendation = typeof event.recommendation === "string" && event.recommendation
1151
+ ? event.recommendation
1152
+ : "Please narrow the scope and continue with a specific target.";
1153
+ const streak = Number(event.consecutive_read_only_iterations ?? 0);
1154
+ const threshold = Number(event.threshold ?? 0);
1155
+ pushMessage("system", `Exploration paused (${streak}/${threshold} read-only turns). ${recommendation}`);
1156
+ return null;
998
1157
  }
999
1158
  if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
1000
1159
  return null;
1001
1160
  }
1002
1161
  if (!isSuperAdmin && event.type === "tool_start") {
1003
- const tool = event.tool;
1004
- const name = tool?.name ?? "tool";
1005
- pushToolCard({
1006
- kind: "start",
1007
- toolName: name,
1008
- locality: "REMOTE",
1009
- summary: `${name} is running...`
1010
- });
1011
1162
  return null;
1012
1163
  }
1013
1164
  const toolLine = eventToToolLine(event);
@@ -1022,11 +1173,22 @@ export const AstraApp = () => {
1022
1173
  else if (event.type === "tool_result") {
1023
1174
  const mark = event.success ? "completed" : "failed";
1024
1175
  const toolName = typeof event.tool_name === "string" ? event.tool_name : "tool";
1176
+ const payload = (event.data ?? {});
1177
+ const resultType = typeof event.result_type === "string" ? event.result_type : "";
1178
+ const alreadyRepresented = resultType === "file_create" ||
1179
+ resultType === "file_edit" ||
1180
+ resultType === "file_delete" ||
1181
+ toolName === "start_preview" ||
1182
+ toolName === "capture_screenshot";
1183
+ if (alreadyRepresented) {
1184
+ return null;
1185
+ }
1186
+ const summary = summarizeToolResult(toolName, payload, Boolean(event.success));
1025
1187
  pushToolCard({
1026
1188
  kind: event.success ? "success" : "error",
1027
1189
  toolName,
1028
- locality: (event.data ?? {}).local === true ? "LOCAL" : "REMOTE",
1029
- summary: `${toolName} ${mark}`
1190
+ locality: payload.local === true ? "LOCAL" : "REMOTE",
1191
+ summary: event.success ? summary : `${toolName} ${mark}`
1030
1192
  });
1031
1193
  }
1032
1194
  }
@@ -1038,19 +1200,31 @@ export const AstraApp = () => {
1038
1200
  }
1039
1201
  }
1040
1202
  return null;
1041
- }, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]);
1203
+ }, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, stopLiveVoice, voiceEnabled, writeLocalFile]);
1042
1204
  const sendPrompt = useCallback(async (rawPrompt) => {
1043
1205
  const text = rawPrompt.trim();
1044
1206
  if (!text || !user || thinking) {
1045
1207
  return;
1046
1208
  }
1047
1209
  if (text === "/help") {
1048
- pushMessage("system", "/new /history /voice /voice on|off|status /settings /settings model <id> /logout /exit");
1210
+ pushMessage("system", "/new /history /voice /dictate on|off|status /tools compact|expanded /settings /settings model <id> /logout /exit");
1211
+ pushMessage("system", "");
1212
+ return;
1213
+ }
1214
+ if (text === "/tools compact") {
1215
+ setToolFeedMode("compact");
1216
+ pushMessage("system", "Tool feed set to compact.");
1217
+ pushMessage("system", "");
1218
+ return;
1219
+ }
1220
+ if (text === "/tools expanded") {
1221
+ setToolFeedMode("expanded");
1222
+ pushMessage("system", "Tool feed set to expanded.");
1049
1223
  pushMessage("system", "");
1050
1224
  return;
1051
1225
  }
1052
1226
  if (text === "/settings") {
1053
- pushMessage("system", `Settings: mode=${runtimeMode} scope=${workspaceRoot} model=${activeModel} provider=${getProviderForModel(activeModel)} voice=${voiceEnabled ? "on" : "off"} listening=${voiceListening ? "yes" : "no"} silence_ms=${VOICE_SILENCE_MS} role=${user.role ?? "user"} client_id=${getDefaultClientId()} backend=${getBackendUrl()}`);
1227
+ pushMessage("system", `Settings: mode=${runtimeMode} scope=${workspaceRoot} model=${activeModel} provider=${getProviderForModel(activeModel)} voice=${voiceEnabled ? "on" : "off"} listening=${voiceListening ? "yes" : "no"} tool_feed=${toolFeedMode} silence_ms=${VOICE_SILENCE_MS} role=${user.role ?? "user"} client_id=${getDefaultClientId()} backend=${getBackendUrl()}`);
1054
1228
  pushMessage("system", "");
1055
1229
  return;
1056
1230
  }
@@ -1081,29 +1255,29 @@ export const AstraApp = () => {
1081
1255
  await openHistory();
1082
1256
  return;
1083
1257
  }
1084
- if (text === "/voice status") {
1085
- pushMessage("system", `Voice input is ${voiceEnabled ? "on" : "off"}${voiceListening ? " (live transcription active)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`);
1258
+ if (text === "/dictate status") {
1259
+ pushMessage("system", `Voice input is ${voiceEnabled ? "on" : "off"}${voicePreparing ? " (preparing mic)" : ""}${voiceListening ? " (listening)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`);
1086
1260
  pushMessage("system", "");
1087
1261
  return;
1088
1262
  }
1089
- if (text === "/voice on") {
1263
+ if (text === "/dictate on") {
1090
1264
  setVoiceEnabled(true);
1091
1265
  startLiveVoice(true);
1092
1266
  pushMessage("system", `Voice input enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
1093
1267
  pushMessage("system", "");
1094
1268
  return;
1095
1269
  }
1096
- if (text === "/voice off") {
1270
+ if (text === "/dictate off") {
1097
1271
  await stopLiveVoice();
1098
1272
  setVoiceEnabled(false);
1099
1273
  pushMessage("system", "Voice input disabled.");
1100
1274
  pushMessage("system", "");
1101
1275
  return;
1102
1276
  }
1103
- if (text === "/voice input") {
1277
+ if (text === "/dictate input") {
1104
1278
  const transcribed = await transcribeOnce();
1105
1279
  if (!transcribed) {
1106
- pushMessage("error", "No speech transcribed. Ensure you're signed in and that mic capture works (optional ASTRA_STT_CAPTURE_COMMAND).");
1280
+ pushMessage("error", "No speech transcribed. Ensure you're signed in and your microphone capture works (optional ASTRA_STT_CAPTURE_COMMAND).");
1107
1281
  return;
1108
1282
  }
1109
1283
  setVoiceQueuedPrompt(transcribed.trim());
@@ -1170,6 +1344,9 @@ export const AstraApp = () => {
1170
1344
  localActionConfirmed = true;
1171
1345
  }
1172
1346
  }
1347
+ if (event.type === "done") {
1348
+ break;
1349
+ }
1173
1350
  const piece = await handleEvent(event, activeSessionId);
1174
1351
  if (piece) {
1175
1352
  assistant += piece;
@@ -1209,9 +1386,11 @@ export const AstraApp = () => {
1209
1386
  startLiveVoice,
1210
1387
  stopLiveVoice,
1211
1388
  thinking,
1389
+ toolFeedMode,
1212
1390
  user,
1213
1391
  voiceEnabled,
1214
1392
  voiceListening,
1393
+ voicePreparing,
1215
1394
  voiceWaitingForSilence,
1216
1395
  workspaceRoot
1217
1396
  ]);
@@ -1224,21 +1403,46 @@ export const AstraApp = () => {
1224
1403
  if (!queued) {
1225
1404
  return;
1226
1405
  }
1406
+ const normalizedQueued = queued.toLowerCase();
1407
+ const last = lastVoicePromptRef.current;
1408
+ const isLikelyNoise = isLikelyVoiceNoise(normalizedQueued);
1409
+ const isFastDuplicate = last !== null &&
1410
+ last.text === normalizedQueued &&
1411
+ Date.now() - last.at <= VOICE_DUPLICATE_WINDOW_MS;
1412
+ // #region agent log
1413
+ fetch('http://127.0.0.1:7573/ingest/fdd4f018-1ba3-4303-b1bb-375443267476', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '17f1ea' }, body: JSON.stringify({ sessionId: '17f1ea', runId: 'voice_run_1', hypothesisId: 'H3', location: 'App.tsx:voiceQueuedPromptEffect', message: 'queued prompt evaluated', data: { queued, normalizedQueued, isLikelyNoise, isFastDuplicate, thinking }, timestamp: Date.now() }) }).catch(() => { });
1414
+ // #endregion
1415
+ if (isLikelyNoise || isFastDuplicate) {
1416
+ const now = Date.now();
1417
+ const lastIgnored = lastIgnoredVoiceRef.current;
1418
+ const shouldLogIgnored = !lastIgnored ||
1419
+ lastIgnored.text !== normalizedQueued ||
1420
+ now - lastIgnored.at > VOICE_DUPLICATE_WINDOW_MS;
1421
+ if (shouldLogIgnored) {
1422
+ pushMessage("system", `Ignored likely-noise dictation input: "${queued}"`);
1423
+ lastIgnoredVoiceRef.current = { text: normalizedQueued, at: now };
1424
+ }
1425
+ return;
1426
+ }
1427
+ lastVoicePromptRef.current = { text: normalizedQueued, at: Date.now() };
1227
1428
  pushMessage("system", `Voice input: ${queued}`);
1228
1429
  setPrompt("");
1430
+ // #region agent log
1431
+ fetch('http://127.0.0.1:7573/ingest/fdd4f018-1ba3-4303-b1bb-375443267476', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '17f1ea' }, body: JSON.stringify({ sessionId: '17f1ea', runId: 'voice_run_1', hypothesisId: 'H3', location: 'App.tsx:voiceQueuedPromptEffect', message: 'queued prompt forwarded to sendPrompt', data: { queuedLen: queued.length }, timestamp: Date.now() }) }).catch(() => { });
1432
+ // #endregion
1229
1433
  void sendPrompt(queued);
1230
1434
  }, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
1231
1435
  if (!trustedWorkspace) {
1232
1436
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#c0c9db", children: "claude" }), _jsx(Text, { color: "#8ea1bd", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#f0f4ff", children: "Do you trust the files in this folder?" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#c8d5f0", children: workspaceRoot }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#8ea1bd", children: "Astra Code may read, write, or execute files contained in this directory. This can pose security risks, so only use files from trusted sources." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#7aa2ff", children: "Learn more" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: trustSelection === 0 ? "#f0f4ff" : "#8ea1bd", children: [trustSelection === 0 ? "❯ " : " ", "1. Yes, proceed"] }), _jsxs(Text, { color: trustSelection === 1 ? "#f0f4ff" : "#8ea1bd", children: [trustSelection === 1 ? "❯ " : " ", "2. No, exit"] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#8ea1bd", children: "Enter to confirm \u00B7 Esc to cancel" }) })] }));
1233
1437
  }
1234
1438
  if (booting) {
1235
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsxs(Text, { color: "#8aa2c9", children: [_jsx(Spinner, { type: "dots12" }), " Booting Astra terminal shell..."] })] }));
1439
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsxs(Text, { color: "#8aa2c9", children: [_jsx(Spinner, { type: "dots12" }), " Booting Astra terminal shell..."] })] }));
1236
1440
  }
1237
1441
  if (bootError) {
1238
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsx(Text, { color: "red", children: bootError })] }));
1442
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsx(Text, { color: "red", children: bootError })] }));
1239
1443
  }
1240
1444
  if (!user) {
1241
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsxs(Text, { color: "#b8c8ff", children: ["Astra terminal AI pair programmer (", loginMode === "login" ? "Sign in" : "Create account", ")"] }), _jsx(Text, { color: "#7c8ea8", children: "Press Ctrl+T to toggle Sign in / Create account" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "#93a3b8", children: "Email: " }), _jsx(TextInput, { value: email, onChange: setEmail, focus: loginField === "email", onSubmit: () => {
1445
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsxs(Text, { color: "#b8c8ff", children: ["Astra terminal AI pair programmer (", loginMode === "login" ? "Sign in" : "Create account", ")"] }), _jsx(Text, { color: "#7c8ea8", children: "Press Ctrl+T to toggle Sign in / Create account" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "#93a3b8", children: "Email: " }), _jsx(TextInput, { value: email, onChange: setEmail, focus: loginField === "email", onSubmit: () => {
1242
1446
  setLoginField("password");
1243
1447
  } })] }), _jsxs(Box, { children: [_jsx(Text, { color: "#93a3b8", children: "Password: " }), _jsx(TextInput, { value: password, onChange: setPassword, mask: "*", focus: loginField === "password", onSubmit: () => {
1244
1448
  void doAuth();
@@ -1246,21 +1450,36 @@ export const AstraApp = () => {
1246
1450
  }
1247
1451
  if (historyOpen) {
1248
1452
  const selected = filteredHistory[historyIndex];
1249
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), historyMode === "picker" ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: "#dce9ff", children: "History Picker" }), _jsx(Text, { color: "#5a7a9a", children: "Esc close \u00B7 Enter select" })] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: historyPickerIndex === 0 ? "#dce9ff" : "#7a9bba", children: [historyPickerIndex === 0 ? "❯ " : " ", "View chat history"] }) }), _jsx(Box, { marginTop: 1, flexDirection: "row", children: _jsxs(Text, { color: historyPickerIndex === 1 ? "#dce9ff" : "#7a9bba", children: [historyPickerIndex === 1 ? "❯ " : " ", "View credit usage history"] }) })] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Text, { color: "#5a7a9a", children: ["Credit usage history opens: ", HISTORY_SETTINGS_URL] })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: "#dce9ff", children: "Chat History" }), _jsx(Text, { color: "#5a7a9a", children: "Esc back \u00B7 Enter open \u00B7 D delete \u00B7 \u2190/\u2192 page" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "search " }), _jsx(TextInput, { value: historyQuery, onChange: setHistoryQuery, placeholder: "Filter chats..." })] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), historyLoading ? (_jsxs(Text, { color: "#8aa2c9", children: [_jsx(Spinner, { type: "dots12" }), " Loading chat history..."] })) : filteredHistory.length === 0 ? (_jsx(Text, { color: "#8ea1bd", children: "No sessions found." })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: pageRows.map((row, localIdx) => {
1453
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsx(Text, { color: "#2a3a50", children: divider }), historyMode === "picker" ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: "#dce9ff", children: "History Picker" }), _jsx(Text, { color: "#5a7a9a", children: "Esc close \u00B7 Enter select" })] }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: historyPickerIndex === 0 ? "#dce9ff" : "#7a9bba", children: [historyPickerIndex === 0 ? "❯ " : " ", "View chat history"] }) }), _jsx(Box, { marginTop: 1, flexDirection: "row", children: _jsxs(Text, { color: historyPickerIndex === 1 ? "#dce9ff" : "#7a9bba", children: [historyPickerIndex === 1 ? "❯ " : " ", "View credit usage history"] }) })] }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsxs(Text, { color: "#5a7a9a", children: ["Credit usage history opens: ", HISTORY_SETTINGS_URL] })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: "#dce9ff", children: "Chat History" }), _jsx(Text, { color: "#5a7a9a", children: "Esc back \u00B7 Enter open \u00B7 D delete \u00B7 \u2190/\u2192 page" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "search " }), _jsx(TextInput, { value: historyQuery, onChange: setHistoryQuery, placeholder: "Filter chats..." })] }), _jsx(Text, { color: "#2a3a50", children: divider }), historyLoading ? (_jsxs(Text, { color: "#8aa2c9", children: [_jsx(Spinner, { type: "dots12" }), " Loading chat history..."] })) : filteredHistory.length === 0 ? (_jsx(Text, { color: "#8ea1bd", children: "No sessions found." })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: pageRows.map((row, localIdx) => {
1250
1454
  const idx = pageStart + localIdx;
1251
1455
  const active = idx === historyIndex;
1252
1456
  return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { color: active ? "#dce9ff" : "#7a9bba", children: [active ? "❯ " : " ", (row.title || "Untitled").slice(0, 58).padEnd(60, " ")] }), _jsxs(Text, { color: "#5a7a9a", children: [String(row.total_messages ?? 0).padStart(3, " "), " msgs \u00B7 ", formatSessionDate(row.updated_at)] })] }, row.id));
1253
- }) })), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { color: "#5a7a9a", children: ["Page ", historyPage + 1, " / ", historyPageCount] }), _jsxs(Text, { color: "#5a7a9a", children: ["Selected: ", selected ? selected.id : "--"] })] })] }))] }));
1457
+ }) })), _jsx(Text, { color: "#2a3a50", children: divider }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { color: "#5a7a9a", children: ["Page ", historyPage + 1, " / ", historyPageCount] }), _jsxs(Text, { color: "#5a7a9a", children: ["Selected: ", selected ? selected.id : "--"] })] })] }))] }));
1254
1458
  }
1255
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "mode " }), _jsx(Text, { color: "#9ad5ff", children: runtimeMode })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "scope " }), _jsx(Text, { color: "#7a9bba", children: workspaceRoot })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "provider " }), _jsx(Text, { color: "#9ad5ff", children: getProviderForModel(activeModel) })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "credits " }), _jsxs(Text, { color: creditsRemaining !== null && creditsRemaining < 50 ? "#ffaa55" : "#9ad5ff", children: [creditsRemaining ?? "--", lastCreditCost !== null ? (_jsxs(Text, { color: "#5a7a9a", children: [" (-", lastCreditCost, ")"] })) : null] })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "model " }), _jsx(Text, { color: "#9ad5ff", children: activeModel })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "voice " }), _jsx(Text, { color: voiceEnabled ? "#9ad5ff" : "#5a7a9a", children: voiceEnabled ? (voiceListening ? (voiceWaitingForSilence ? "on/waiting" : "on/listening") : "on") : "off" })] })] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsx(Text, { color: "#3a5068", children: "/help /new /history /voice /voice on|off|status /settings /logout /exit" }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [messages.map((message, index) => {
1459
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsx(Text, { color: "#7a9bba", children: `mode ${runtimeMode} · provider ${getProviderForModel(activeModel)} · model ${activeModel} · credits ${creditsRemaining ?? "--"}${lastCreditCost !== null ? ` (-${lastCreditCost})` : ""}` }), _jsx(Text, { color: "#6c88a8", children: `scope ${workspaceRoot.length > HEADER_PATH_MAX ? `…${workspaceRoot.slice(-(HEADER_PATH_MAX - 1))}` : workspaceRoot} · voice ${voiceEnabled
1460
+ ? voicePreparing
1461
+ ? "on/preparing"
1462
+ : voiceListening
1463
+ ? voiceWaitingForSilence
1464
+ ? "on/waiting"
1465
+ : "on/listening"
1466
+ : "on/standby"
1467
+ : "off"}` }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsx(Text, { color: "#3a5068", children: "/help /new /history /voice /dictate on|off|status" }), _jsx(Text, { color: "#3a5068", children: "/tools compact|expanded /settings /logout /exit" }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [messages.map((message, index) => {
1468
+ const prev = index > 0 ? messages[index - 1] : null;
1256
1469
  const style = styleForKind(message.kind);
1257
1470
  const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
1258
1471
  const isSpacing = message.text === "" && message.kind === "system";
1472
+ const needsGroupGap = Boolean(prev) &&
1473
+ prev?.kind !== "system" &&
1474
+ message.kind !== "system" &&
1475
+ prev?.kind !== message.kind &&
1476
+ (message.kind === "assistant" || message.kind === "tool");
1259
1477
  if (isSpacing) {
1260
1478
  return _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: " " }) }, `${index}-spacer`);
1261
1479
  }
1262
1480
  if (message.kind === "tool" && message.card && !isSuperAdmin) {
1263
1481
  const card = message.card;
1482
+ const localityLabel = card.locality === "LOCAL" ? "local" : "cloud";
1264
1483
  const icon = card.kind === "error"
1265
1484
  ? "✕"
1266
1485
  : card.kind === "fileDelete"
@@ -1287,10 +1506,10 @@ export const AstraApp = () => {
1287
1506
  : card.kind === "preview"
1288
1507
  ? "#9ad5ff"
1289
1508
  : "#9bc5ff";
1290
- return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, children: paddedLabel }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: accent, children: [icon, " ", card.summary, " ", _jsxs(Text, { color: "#5a7a9a", children: ["[", card.locality, "]"] })] }), card.path ? _jsxs(Text, { color: "#6c88a8", children: ["path: ", card.path] }) : null, (card.snippetLines ?? []).slice(0, TOOL_SNIPPET_LINES).map((line, idx) => (_jsx(Text, { color: "#8ea1bd", children: line }, `${index}-snippet-${idx}`)))] })] }, `${index}-${message.kind}`));
1509
+ return (_jsxs(React.Fragment, { children: [needsGroupGap ? _jsx(Text, { children: " " }) : null, _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, children: paddedLabel }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: accent, children: [icon, " ", card.summary, card.count && card.count > 1 ? _jsxs(Text, { color: "#9cb8d8", children: [" (x", card.count, ")"] }) : null, _jsxs(Text, { color: "#5a7a9a", children: [" \u00B7 ", localityLabel] })] }), toolFeedMode === "expanded" ? (_jsxs(_Fragment, { children: [card.path ? _jsxs(Text, { color: "#6c88a8", children: ["path: ", card.path] }) : null, (card.snippetLines ?? []).slice(0, TOOL_SNIPPET_LINES).map((line, idx) => (_jsx(Text, { color: "#8ea1bd", children: line }, `${index}-snippet-${idx}`)))] })) : null] })] })] }, `${index}-${message.kind}`));
1291
1510
  }
1292
- return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, bold: style.bold, children: paddedLabel }), message.kind === "assistant" ? (_jsx(Box, { flexDirection: "column", children: renderMarkdownContent(message.text, style.textColor, `assistant-${index}`) })) : (_jsx(Text, { color: style.textColor, bold: style.bold && message.kind === "error", children: message.text }))] }, `${index}-${message.kind}`));
1293
- }), streamingText ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: styleForKind("assistant").label.padEnd(LABEL_WIDTH, " ") }), _jsx(Box, { flexDirection: "column", children: renderMarkdownContent(streamingText, "#dce9ff", "streaming") })] })) : null] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#7aa2ff", children: "◆ astra".padEnd(LABEL_WIDTH, " ") }), _jsxs(Text, { color: "#6080a0", children: [_jsx(Spinner, { type: "dots2" }), _jsx(Text, { color: "#8aa2c9", children: " thinking..." })] })] })) : null, voiceEnabled && !thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#9ad5ff", children: "🎤 voice".padEnd(LABEL_WIDTH, " ") }), voiceListening && !voiceWaitingForSilence ? (_jsx(Text, { color: "#a6d9ff", children: "\uD83C\uDF99 listening... goblin ears activated \uD83D\uDC42" })) : voiceWaitingForSilence ? (_jsx(Text, { color: "#b7c4d8", children: "\u23F8 waiting for silence... dramatic pause loading..." })) : (_jsx(Text, { color: "#6f8199", children: "voice armed... say something when ready" }))] })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: "\u276F " }), _jsx(TextInput, { value: prompt, onSubmit: (value) => {
1511
+ return (_jsxs(React.Fragment, { children: [needsGroupGap ? _jsx(Text, { children: " " }) : null, _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, bold: style.bold, children: paddedLabel }), message.kind === "assistant" ? (_jsx(Box, { flexDirection: "column", children: renderMarkdownContent(message.text, style.textColor, `assistant-${index}`) })) : (_jsx(Text, { color: style.textColor, bold: style.bold && message.kind === "error", children: message.text }))] })] }, `${index}-${message.kind}`));
1512
+ }), streamingText ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: styleForKind("assistant").label.padEnd(LABEL_WIDTH, " ") }), _jsx(Box, { flexDirection: "column", children: renderMarkdownContent(streamingText, "#dce9ff", "streaming") })] })) : null] }), _jsx(Text, { color: "#2a3a50", children: divider }), thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#7aa2ff", children: "◆ astra".padEnd(LABEL_WIDTH, " ") }), _jsxs(Text, { color: "#6080a0", children: [_jsx(Spinner, { type: "dots2" }), _jsx(Text, { color: "#8aa2c9", children: " thinking..." })] })] })) : null, voiceEnabled && !thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#9ad5ff", children: "🎤 voice".padEnd(LABEL_WIDTH, " ") }), voicePreparing ? (_jsx(Text, { color: "#f4d58a", children: "\uD83D\uDFE1 preparing microphone..." })) : voiceListening && !voiceWaitingForSilence ? (_jsx(Text, { color: "#9de3b4", children: "\uD83D\uDFE2 listening now - speak clearly" })) : voiceWaitingForSilence ? (_jsx(Text, { color: "#b7c4d8", children: "\u23F3 speech detected - waiting for silence to send" })) : (_jsx(Text, { color: "#6f8199", children: "\u26AA voice armed - preparing next listen window" }))] })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: "\u276F " }), _jsx(TextInput, { value: prompt, onSubmit: (value) => {
1294
1513
  setPrompt("");
1295
1514
  void sendPrompt(value);
1296
1515
  }, onChange: (value) => {