@astra-code/astra-ai 0.1.4 → 0.1.6

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/dist/app/App.js CHANGED
@@ -4,7 +4,7 @@ import { Box, Text, useApp, useInput } from "ink";
4
4
  import Spinner from "ink-spinner";
5
5
  import TextInput from "ink-text-input";
6
6
  import { spawn } from "child_process";
7
- import { mkdirSync, unlinkSync, writeFileSync } from "fs";
7
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
8
8
  import { dirname, join } from "path";
9
9
  import { BackendClient } from "../lib/backendClient.js";
10
10
  import { clearSession, loadSession, saveSession } from "../lib/sessionStore.js";
@@ -12,7 +12,7 @@ import { getBackendUrl, getDefaultClientId, getDefaultModel, getProviderForModel
12
12
  import { runTerminalCommand } from "../lib/terminalBridge.js";
13
13
  import { isWorkspaceTrusted, trustWorkspace } from "../lib/trustStore.js";
14
14
  import { scanWorkspace } from "../lib/workspaceScanner.js";
15
- import { startLiveTranscription, transcribeOnce } from "../lib/voice.js";
15
+ import { startLiveTranscription, transcribeOnce, resolveAudioDevice, setAudioDevice, listAvfAudioDevices, writeAstraKey } from "../lib/voice.js";
16
16
  // const ASTRA_ASCII = `
17
17
  // █████╗ ███████╗████████╗██████╗ █████╗ ██████╗ ██████╗ ██████╗ ███████╗
18
18
  // ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
@@ -249,6 +249,24 @@ const summarizeToolResult = (toolName, data, success) => {
249
249
  }
250
250
  return `${toolName} completed`;
251
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;
269
+ };
252
270
  const parseInline = (line) => {
253
271
  const tokens = [];
254
272
  const re = /(`[^`]+`|\*\*[^*]+\*\*|__[^_]+__|\*[^*\n]+\*|_[^_\n]+_)/g;
@@ -474,6 +492,7 @@ export const AstraApp = () => {
474
492
  const [voicePreparing, setVoicePreparing] = useState(false);
475
493
  const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
476
494
  const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState(null);
495
+ const [micSetupDevices, setMicSetupDevices] = useState(null);
477
496
  const [toolFeedMode, setToolFeedMode] = useState("compact");
478
497
  const [historyOpen, setHistoryOpen] = useState(false);
479
498
  const [historyMode, setHistoryMode] = useState("picker");
@@ -596,57 +615,96 @@ export const AstraApp = () => {
596
615
  if (liveVoiceRef.current) {
597
616
  return;
598
617
  }
618
+ // #region agent log
619
+ 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(() => { });
620
+ // #endregion
599
621
  setVoiceEnabled(true);
600
622
  setVoicePreparing(true);
601
623
  setVoiceListening(false);
602
624
  setVoiceWaitingForSilence(false);
603
625
  if (announce) {
604
- pushMessage("system", "Dictation armed. Preparing microphone...");
626
+ pushMessage("system", "Voice input armed. Preparing microphone...");
605
627
  }
606
- liveVoiceRef.current = startLiveTranscription({
607
- onPartial: (text) => {
628
+ // Resolve mic device before starting — triggers onboarding if not configured.
629
+ void resolveAudioDevice(workspaceRoot).then((device) => {
630
+ if (device === null) {
631
+ // No device configured — run onboarding inline.
608
632
  setVoicePreparing(false);
609
- setVoiceListening(true);
610
- setPrompt(text);
611
- if (voiceSilenceTimerRef.current) {
612
- clearTimeout(voiceSilenceTimerRef.current);
613
- }
614
- const candidate = text.trim();
615
- if (!candidate) {
616
- setVoiceWaitingForSilence(false);
633
+ const devices = listAvfAudioDevices();
634
+ if (!devices.length) {
635
+ pushMessage("error", "No audio devices found. Install ffmpeg: brew install ffmpeg");
636
+ setVoiceEnabled(false);
617
637
  return;
618
638
  }
619
- const normalized = candidate.toLowerCase();
620
- const isLikelyNoise = normalized.length < VOICE_MIN_CHARS || VOICE_NOISE_WORDS.has(normalized);
621
- if (isLikelyNoise) {
639
+ setMicSetupDevices(devices);
640
+ const lines = [
641
+ "Let's set up your microphone first.",
642
+ ...devices.map(d => ` [${d.index}] ${d.name}`),
643
+ "Type the number for your mic and press Enter:"
644
+ ];
645
+ pushMessage("system", lines.join("\n"));
646
+ return;
647
+ }
648
+ // Device resolved — start transcription.
649
+ liveVoiceRef.current = startLiveTranscription({
650
+ onPartial: (text) => {
651
+ setVoicePreparing(false);
652
+ setVoiceListening(true);
653
+ if (voiceSilenceTimerRef.current) {
654
+ clearTimeout(voiceSilenceTimerRef.current);
655
+ }
656
+ const candidate = text.trim();
657
+ if (!candidate) {
658
+ setPrompt("");
659
+ setVoiceWaitingForSilence(false);
660
+ return;
661
+ }
662
+ const normalized = candidate.toLowerCase();
663
+ const isLikelyNoise = isLikelyVoiceNoise(normalized);
664
+ // #region agent log
665
+ 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(() => { });
666
+ // #endregion
667
+ if (isLikelyNoise) {
668
+ setPrompt("");
669
+ setVoiceWaitingForSilence(false);
670
+ return;
671
+ }
672
+ setPrompt(text);
673
+ setVoiceWaitingForSilence(true);
674
+ voiceSilenceTimerRef.current = setTimeout(() => {
675
+ // #region agent log
676
+ 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(() => { });
677
+ // #endregion
678
+ setVoiceQueuedPrompt(candidate);
679
+ void stopLiveVoice();
680
+ }, VOICE_SILENCE_MS);
681
+ },
682
+ onFinal: (text) => {
683
+ if (voiceSilenceTimerRef.current) {
684
+ clearTimeout(voiceSilenceTimerRef.current);
685
+ voiceSilenceTimerRef.current = null;
686
+ }
687
+ setPrompt(text);
688
+ liveVoiceRef.current = null;
689
+ setVoicePreparing(false);
690
+ setVoiceListening(false);
622
691
  setVoiceWaitingForSilence(false);
623
- return;
624
- }
625
- setVoiceWaitingForSilence(true);
626
- voiceSilenceTimerRef.current = setTimeout(() => {
627
- setVoiceQueuedPrompt(candidate);
628
- void stopLiveVoice();
629
- }, VOICE_SILENCE_MS);
630
- },
631
- onFinal: (text) => {
632
- if (voiceSilenceTimerRef.current) {
633
- clearTimeout(voiceSilenceTimerRef.current);
634
- voiceSilenceTimerRef.current = null;
692
+ // #region agent log
693
+ 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(() => { });
694
+ // #endregion
695
+ },
696
+ onError: (error) => {
697
+ setVoicePreparing(false);
698
+ setVoiceListening(false);
699
+ setVoiceWaitingForSilence(false);
700
+ pushMessage("error", `Voice transcription error: ${error.message}`);
701
+ // #region agent log
702
+ 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(() => { });
703
+ // #endregion
635
704
  }
636
- setPrompt(text);
637
- liveVoiceRef.current = null;
638
- setVoicePreparing(false);
639
- setVoiceListening(false);
640
- setVoiceWaitingForSilence(false);
641
- },
642
- onError: (error) => {
643
- setVoicePreparing(false);
644
- setVoiceListening(false);
645
- setVoiceWaitingForSilence(false);
646
- pushMessage("error", `Voice transcription error: ${error.message}`);
647
- }
648
- });
649
- }, [pushMessage, stopLiveVoice]);
705
+ });
706
+ }); // end resolveAudioDevice.then
707
+ }, [pushMessage, stopLiveVoice, workspaceRoot]);
650
708
  useEffect(() => {
651
709
  return () => {
652
710
  if (voiceSilenceTimerRef.current) {
@@ -728,6 +786,13 @@ export const AstraApp = () => {
728
786
  if (key.return) {
729
787
  if (trustSelection === 0) {
730
788
  trustWorkspace(workspaceRoot);
789
+ // Create .astra settings file at workspace root if it doesn't exist yet.
790
+ try {
791
+ const astraPath = join(workspaceRoot, ".astra");
792
+ if (!existsSync(astraPath))
793
+ writeFileSync(astraPath, "");
794
+ }
795
+ catch { /* non-fatal */ }
731
796
  setTrustedWorkspace(true);
732
797
  setBooting(true);
733
798
  return;
@@ -1107,7 +1172,7 @@ export const AstraApp = () => {
1107
1172
  if (voiceEnabled) {
1108
1173
  await stopLiveVoice();
1109
1174
  setVoiceEnabled(false);
1110
- pushMessage("system", "Dictation paused: credits exhausted. Recharge, then run /dictate on.");
1175
+ pushMessage("system", "Voice input paused: credits exhausted. Recharge, then run /voice on.");
1111
1176
  pushMessage("system", "");
1112
1177
  }
1113
1178
  }
@@ -1171,8 +1236,29 @@ export const AstraApp = () => {
1171
1236
  if (!text || !user || thinking) {
1172
1237
  return;
1173
1238
  }
1239
+ // Mic onboarding: intercept when waiting for device selection.
1240
+ if (micSetupDevices !== null) {
1241
+ const idx = parseInt(text, 10);
1242
+ const valid = !isNaN(idx) && idx >= 0 && micSetupDevices.some(d => d.index === idx);
1243
+ if (!valid) {
1244
+ pushMessage("error", `Please type one of: ${micSetupDevices.map(d => d.index).join(", ")}`);
1245
+ return;
1246
+ }
1247
+ const device = `:${idx}`;
1248
+ // Write to .astra local cache
1249
+ writeAstraKey(workspaceRoot, "ASTRA_STT_DEVICE", device);
1250
+ // Persist to backend
1251
+ void backend.updateCliSettings({ audio_device_index: idx });
1252
+ // Update in-process cache
1253
+ setAudioDevice(device);
1254
+ setMicSetupDevices(null);
1255
+ pushMessage("system", `Mic set to [${idx}] ${micSetupDevices.find(d => d.index === idx)?.name ?? ""}. Starting voice...`);
1256
+ pushMessage("system", "");
1257
+ startLiveVoice(false);
1258
+ return;
1259
+ }
1174
1260
  if (text === "/help") {
1175
- pushMessage("system", "/new /history /dictate /dictate on|off|status /tools compact|expanded /settings /settings model <id> /logout /exit");
1261
+ pushMessage("system", "/new /history /voice /dictate on|off|status /tools compact|expanded /settings /settings model <id> /logout /exit");
1176
1262
  pushMessage("system", "");
1177
1263
  return;
1178
1264
  }
@@ -1189,17 +1275,17 @@ export const AstraApp = () => {
1189
1275
  return;
1190
1276
  }
1191
1277
  if (text === "/settings") {
1192
- pushMessage("system", `Settings: mode=${runtimeMode} scope=${workspaceRoot} model=${activeModel} provider=${getProviderForModel(activeModel)} dictate=${voiceEnabled ? "on" : "off"} listening=${voiceListening ? "yes" : "no"} tool_feed=${toolFeedMode} silence_ms=${VOICE_SILENCE_MS} role=${user.role ?? "user"} client_id=${getDefaultClientId()} backend=${getBackendUrl()}`);
1278
+ 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()}`);
1193
1279
  pushMessage("system", "");
1194
1280
  return;
1195
1281
  }
1196
- if (text === "/dictate" || text === "/voice") {
1282
+ if (text === "/voice") {
1197
1283
  if (!voiceEnabled) {
1198
1284
  setVoiceEnabled(true);
1199
1285
  startLiveVoice(true);
1200
1286
  return;
1201
1287
  }
1202
- pushMessage("system", `Dictation is on${voiceListening ? " (currently listening)" : ""}. Use /dictate off to disable.`);
1288
+ pushMessage("system", `Voice input is on${voiceListening ? " (currently listening)" : ""}. Use /voice off to disable.`);
1203
1289
  pushMessage("system", "");
1204
1290
  return;
1205
1291
  }
@@ -1220,26 +1306,26 @@ export const AstraApp = () => {
1220
1306
  await openHistory();
1221
1307
  return;
1222
1308
  }
1223
- if (text === "/dictate status" || text === "/voice status") {
1224
- pushMessage("system", `Dictation is ${voiceEnabled ? "on" : "off"}${voicePreparing ? " (preparing mic)" : ""}${voiceListening ? " (listening)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`);
1309
+ if (text === "/dictate status") {
1310
+ pushMessage("system", `Voice input is ${voiceEnabled ? "on" : "off"}${voicePreparing ? " (preparing mic)" : ""}${voiceListening ? " (listening)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`);
1225
1311
  pushMessage("system", "");
1226
1312
  return;
1227
1313
  }
1228
- if (text === "/dictate on" || text === "/voice on") {
1314
+ if (text === "/dictate on") {
1229
1315
  setVoiceEnabled(true);
1230
1316
  startLiveVoice(true);
1231
- pushMessage("system", `Dictation enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
1317
+ pushMessage("system", `Voice input enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
1232
1318
  pushMessage("system", "");
1233
1319
  return;
1234
1320
  }
1235
- if (text === "/dictate off" || text === "/voice off") {
1321
+ if (text === "/dictate off") {
1236
1322
  await stopLiveVoice();
1237
1323
  setVoiceEnabled(false);
1238
- pushMessage("system", "Dictation disabled.");
1324
+ pushMessage("system", "Voice input disabled.");
1239
1325
  pushMessage("system", "");
1240
1326
  return;
1241
1327
  }
1242
- if (text === "/dictate input" || text === "/voice input") {
1328
+ if (text === "/dictate input") {
1243
1329
  const transcribed = await transcribeOnce();
1244
1330
  if (!transcribed) {
1245
1331
  pushMessage("error", "No speech transcribed. Ensure you're signed in and your microphone capture works (optional ASTRA_STT_CAPTURE_COMMAND).");
@@ -1345,9 +1431,11 @@ export const AstraApp = () => {
1345
1431
  exit,
1346
1432
  handleEvent,
1347
1433
  localFileCache,
1434
+ micSetupDevices,
1348
1435
  openHistory,
1349
1436
  pushMessage,
1350
1437
  sessionId,
1438
+ setMicSetupDevices,
1351
1439
  startLiveVoice,
1352
1440
  stopLiveVoice,
1353
1441
  thinking,
@@ -1370,10 +1458,13 @@ export const AstraApp = () => {
1370
1458
  }
1371
1459
  const normalizedQueued = queued.toLowerCase();
1372
1460
  const last = lastVoicePromptRef.current;
1373
- const isLikelyNoise = normalizedQueued.length < VOICE_MIN_CHARS || VOICE_NOISE_WORDS.has(normalizedQueued);
1461
+ const isLikelyNoise = isLikelyVoiceNoise(normalizedQueued);
1374
1462
  const isFastDuplicate = last !== null &&
1375
1463
  last.text === normalizedQueued &&
1376
1464
  Date.now() - last.at <= VOICE_DUPLICATE_WINDOW_MS;
1465
+ // #region agent log
1466
+ 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(() => { });
1467
+ // #endregion
1377
1468
  if (isLikelyNoise || isFastDuplicate) {
1378
1469
  const now = Date.now();
1379
1470
  const lastIgnored = lastIgnoredVoiceRef.current;
@@ -1387,8 +1478,11 @@ export const AstraApp = () => {
1387
1478
  return;
1388
1479
  }
1389
1480
  lastVoicePromptRef.current = { text: normalizedQueued, at: Date.now() };
1390
- pushMessage("system", `Dictation input: ${queued}`);
1481
+ pushMessage("system", `Voice input: ${queued}`);
1391
1482
  setPrompt("");
1483
+ // #region agent log
1484
+ 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(() => { });
1485
+ // #endregion
1392
1486
  void sendPrompt(queued);
1393
1487
  }, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
1394
1488
  if (!trustedWorkspace) {
@@ -1415,7 +1509,7 @@ export const AstraApp = () => {
1415
1509
  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));
1416
1510
  }) })), _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 : "--"] })] })] }))] }));
1417
1511
  }
1418
- 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} · dictate ${voiceEnabled
1512
+ 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
1419
1513
  ? voicePreparing
1420
1514
  ? "on/preparing"
1421
1515
  : voiceListening
@@ -1423,7 +1517,7 @@ export const AstraApp = () => {
1423
1517
  ? "on/waiting"
1424
1518
  : "on/listening"
1425
1519
  : "on/standby"
1426
- : "off"}` }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsx(Text, { color: "#3a5068", children: "/help /new /history /dictate /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) => {
1520
+ : "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) => {
1427
1521
  const prev = index > 0 ? messages[index - 1] : null;
1428
1522
  const style = styleForKind(message.kind);
1429
1523
  const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
@@ -1468,13 +1562,13 @@ export const AstraApp = () => {
1468
1562
  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}`));
1469
1563
  }
1470
1564
  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}`));
1471
- }), 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: "🎤 dictate".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) => {
1565
+ }), 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) => {
1472
1566
  setPrompt("");
1473
1567
  void sendPrompt(value);
1474
1568
  }, onChange: (value) => {
1475
1569
  if (!voiceListening) {
1476
1570
  setPrompt(value);
1477
1571
  }
1478
- }, placeholder: voiceEnabled ? "Ask Astra... (dictate on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
1572
+ }, placeholder: voiceEnabled ? "Ask Astra... (voice on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
1479
1573
  };
1480
1574
  //# sourceMappingURL=App.js.map