@astra-code/astra-ai 0.1.5 → 0.1.7
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 +287 -100
- package/dist/app/App.js.map +1 -1
- package/dist/lib/backendClient.js +105 -17
- package/dist/lib/backendClient.js.map +1 -1
- package/dist/lib/voice.js +93 -1
- package/dist/lib/voice.js.map +1 -1
- package/package.json +1 -1
- package/src/app/App.tsx +287 -82
- package/src/lib/backendClient.ts +106 -17
- package/src/lib/voice.ts +97 -1
- package/src/types/events.ts +1 -0
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
|
// ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
|
@@ -151,6 +151,14 @@ const extractAssistantText = (event) => {
|
|
|
151
151
|
};
|
|
152
152
|
const LABEL_WIDTH = 10;
|
|
153
153
|
const HEADER_PATH_MAX = Number(process.env.ASTRA_HEADER_PATH_MAX ?? "42");
|
|
154
|
+
const formatTime = (ts) => {
|
|
155
|
+
const d = new Date(ts);
|
|
156
|
+
const raw = d.getHours();
|
|
157
|
+
const ampm = raw >= 12 ? "pm" : "am";
|
|
158
|
+
const h = raw % 12 === 0 ? 12 : raw % 12;
|
|
159
|
+
const m = d.getMinutes().toString().padStart(2, "0");
|
|
160
|
+
return `${h}:${m} ${ampm}`;
|
|
161
|
+
};
|
|
154
162
|
const MIN_DIVIDER = 64;
|
|
155
163
|
const MAX_DIVIDER = 120;
|
|
156
164
|
const styleForKind = (kind) => {
|
|
@@ -193,7 +201,13 @@ const normalizeAssistantText = (input) => {
|
|
|
193
201
|
// Remove control chars but preserve newlines/tabs.
|
|
194
202
|
.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, "")
|
|
195
203
|
.replace(/([a-z0-9/])([A-Z])/g, "$1 $2")
|
|
196
|
-
|
|
204
|
+
// Add space after sentence-ending punctuation only when followed by an
|
|
205
|
+
// uppercase letter (sentence start). Using [A-Za-z] here would break
|
|
206
|
+
// file extensions like .css, .json, .jsx, .tsx — those always start with
|
|
207
|
+
// a lowercase letter.
|
|
208
|
+
.replace(/([.!?])([A-Z])/g, "$1 $2")
|
|
209
|
+
// For ! and ? followed by lowercase, also add a space (natural English).
|
|
210
|
+
.replace(/([!?])([a-z])/g, "$1 $2")
|
|
197
211
|
.replace(/([a-z])(\u2022)/g, "$1\n$2")
|
|
198
212
|
.replace(/([^\n])(Summary:|Next:|Tests:)/g, "$1\n\n$2")
|
|
199
213
|
.replace(/([A-Za-z0-9_/-]+)\.\s+md\b/g, "$1.md")
|
|
@@ -220,34 +234,56 @@ const normalizeAssistantText = (input) => {
|
|
|
220
234
|
}
|
|
221
235
|
return deduped.join("\n\n").trim();
|
|
222
236
|
};
|
|
237
|
+
const guessDevUrl = (command) => {
|
|
238
|
+
// Extract an explicit --port or -p value from the command.
|
|
239
|
+
const portMatch = command.match(/(?:--port|-p)\s+(\d+)/) ?? command.match(/(?:--port=|-p=)(\d+)/);
|
|
240
|
+
if (portMatch) {
|
|
241
|
+
return `http://localhost:${portMatch[1]}`;
|
|
242
|
+
}
|
|
243
|
+
// Default ports by framework.
|
|
244
|
+
if (/next/.test(command))
|
|
245
|
+
return "http://localhost:3000";
|
|
246
|
+
if (/vite|vue/.test(command))
|
|
247
|
+
return "http://localhost:5173";
|
|
248
|
+
if (/remix/.test(command))
|
|
249
|
+
return "http://localhost:3000";
|
|
250
|
+
if (/astro/.test(command))
|
|
251
|
+
return "http://localhost:4321";
|
|
252
|
+
if (/angular|ng\s+serve/.test(command))
|
|
253
|
+
return "http://localhost:4200";
|
|
254
|
+
if (/npm\s+run\s+dev|npm\s+start|npx\s+react-scripts/.test(command))
|
|
255
|
+
return "http://localhost:3000";
|
|
256
|
+
return null;
|
|
257
|
+
};
|
|
223
258
|
const summarizeToolResult = (toolName, data, success) => {
|
|
224
259
|
if (!success) {
|
|
225
|
-
return `${toolName} failed
|
|
260
|
+
return { summary: `${toolName} failed` };
|
|
226
261
|
}
|
|
227
262
|
const path = typeof data.path === "string" ? data.path : "";
|
|
228
263
|
const totalLines = typeof data.total_lines === "number" ? data.total_lines : null;
|
|
229
264
|
if (toolName === "view_file") {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
if (path)
|
|
234
|
-
|
|
235
|
-
|
|
265
|
+
const result = {
|
|
266
|
+
summary: totalLines !== null ? `Read ${totalLines} lines` : "Read file",
|
|
267
|
+
};
|
|
268
|
+
if (path)
|
|
269
|
+
result.path = path;
|
|
270
|
+
return result;
|
|
236
271
|
}
|
|
237
272
|
if (toolName === "list_directory") {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
return `Listed <${dir}>`;
|
|
273
|
+
return {
|
|
274
|
+
summary: totalLines !== null ? `Listed ${totalLines} entries` : "Listed directory",
|
|
275
|
+
path: path || ".",
|
|
276
|
+
};
|
|
243
277
|
}
|
|
244
278
|
if (toolName === "semantic_search") {
|
|
245
|
-
return "Searched codebase context";
|
|
279
|
+
return { summary: "Searched codebase context" };
|
|
246
280
|
}
|
|
247
281
|
if (toolName === "search_files") {
|
|
248
|
-
return
|
|
282
|
+
return {
|
|
283
|
+
summary: totalLines !== null ? `Found ${totalLines} matching paths` : "Searched file paths",
|
|
284
|
+
};
|
|
249
285
|
}
|
|
250
|
-
return `${toolName} completed
|
|
286
|
+
return { summary: `${toolName} completed` };
|
|
251
287
|
};
|
|
252
288
|
const isLikelyVoiceNoise = (text) => {
|
|
253
289
|
const normalized = text.trim().toLowerCase();
|
|
@@ -442,9 +478,17 @@ export const AstraApp = () => {
|
|
|
442
478
|
const welcomeLine = dividerWidth < 96 ? "Welcome to Astra" : FOUNDER_WELCOME;
|
|
443
479
|
const backend = useMemo(() => new BackendClient(), []);
|
|
444
480
|
const { exit } = useApp();
|
|
481
|
+
// Keep backend token fresh: when the client auto-refreshes on 401, persist and update state.
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
backend.setOnTokenRefreshed((refreshed) => {
|
|
484
|
+
saveSession(refreshed);
|
|
485
|
+
setUser(refreshed);
|
|
486
|
+
});
|
|
487
|
+
}, [backend]);
|
|
445
488
|
// In-session file cache: tracks files created/edited so subsequent requests
|
|
446
489
|
// include their latest content in workspaceFiles (VirtualFS stays up to date).
|
|
447
490
|
const localFileCache = useRef(new Map());
|
|
491
|
+
const abortRunRef = useRef(null);
|
|
448
492
|
const writeLocalFile = useCallback((relPath, content, language) => {
|
|
449
493
|
try {
|
|
450
494
|
const abs = join(workspaceRoot, relPath);
|
|
@@ -483,6 +527,7 @@ export const AstraApp = () => {
|
|
|
483
527
|
const [activeModel, setActiveModel] = useState(getDefaultModel());
|
|
484
528
|
const [creditsRemaining, setCreditsRemaining] = useState(null);
|
|
485
529
|
const [lastCreditCost, setLastCreditCost] = useState(null);
|
|
530
|
+
const [loopCtx, setLoopCtx] = useState(null);
|
|
486
531
|
const runtimeMode = getRuntimeMode();
|
|
487
532
|
const [prompt, setPrompt] = useState("");
|
|
488
533
|
const [thinking, setThinking] = useState(false);
|
|
@@ -492,7 +537,9 @@ export const AstraApp = () => {
|
|
|
492
537
|
const [voicePreparing, setVoicePreparing] = useState(false);
|
|
493
538
|
const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
|
|
494
539
|
const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState(null);
|
|
540
|
+
const [micSetupDevices, setMicSetupDevices] = useState(null);
|
|
495
541
|
const [toolFeedMode, setToolFeedMode] = useState("compact");
|
|
542
|
+
const [thinkingColorIdx, setThinkingColorIdx] = useState(0);
|
|
496
543
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
497
544
|
const [historyMode, setHistoryMode] = useState("picker");
|
|
498
545
|
const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
|
|
@@ -507,11 +554,11 @@ export const AstraApp = () => {
|
|
|
507
554
|
const fileEditBuffersRef = useRef(new Map());
|
|
508
555
|
const isSuperAdmin = user?.role === "super_admin";
|
|
509
556
|
const pushMessage = useCallback((kind, text) => {
|
|
510
|
-
setMessages((prev) => [...prev, { kind, text }].slice(-300));
|
|
557
|
+
setMessages((prev) => [...prev, { kind, text, ts: Date.now() }].slice(-300));
|
|
511
558
|
}, []);
|
|
512
559
|
const pushToolCard = useCallback((card) => {
|
|
513
560
|
setMessages((prev) => {
|
|
514
|
-
const nextEntry = { kind: "tool", text: card.summary, card };
|
|
561
|
+
const nextEntry = { kind: "tool", text: card.summary, card, ts: Date.now() };
|
|
515
562
|
const last = prev[prev.length - 1];
|
|
516
563
|
if (last &&
|
|
517
564
|
last.kind === "tool" &&
|
|
@@ -624,64 +671,86 @@ export const AstraApp = () => {
|
|
|
624
671
|
if (announce) {
|
|
625
672
|
pushMessage("system", "Voice input armed. Preparing microphone...");
|
|
626
673
|
}
|
|
627
|
-
|
|
628
|
-
|
|
674
|
+
// Resolve mic device before starting — triggers onboarding if not configured.
|
|
675
|
+
void resolveAudioDevice(workspaceRoot).then((device) => {
|
|
676
|
+
if (device === null) {
|
|
677
|
+
// No device configured — run onboarding inline.
|
|
629
678
|
setVoicePreparing(false);
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const candidate = text.trim();
|
|
635
|
-
if (!candidate) {
|
|
636
|
-
setPrompt("");
|
|
637
|
-
setVoiceWaitingForSilence(false);
|
|
679
|
+
const devices = listAvfAudioDevices();
|
|
680
|
+
if (!devices.length) {
|
|
681
|
+
pushMessage("error", "No audio devices found. Install ffmpeg: brew install ffmpeg");
|
|
682
|
+
setVoiceEnabled(false);
|
|
638
683
|
return;
|
|
639
684
|
}
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
685
|
+
setMicSetupDevices(devices);
|
|
686
|
+
const lines = [
|
|
687
|
+
"Let's set up your microphone first.",
|
|
688
|
+
...devices.map(d => ` [${d.index}] ${d.name}`),
|
|
689
|
+
"Type the number for your mic and press Enter:"
|
|
690
|
+
];
|
|
691
|
+
pushMessage("system", lines.join("\n"));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
// Device resolved — start transcription.
|
|
695
|
+
liveVoiceRef.current = startLiveTranscription({
|
|
696
|
+
onPartial: (text) => {
|
|
697
|
+
setVoicePreparing(false);
|
|
698
|
+
setVoiceListening(true);
|
|
699
|
+
if (voiceSilenceTimerRef.current) {
|
|
700
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
701
|
+
}
|
|
702
|
+
const candidate = text.trim();
|
|
703
|
+
if (!candidate) {
|
|
704
|
+
setPrompt("");
|
|
705
|
+
setVoiceWaitingForSilence(false);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const normalized = candidate.toLowerCase();
|
|
709
|
+
const isLikelyNoise = isLikelyVoiceNoise(normalized);
|
|
710
|
+
// #region agent log
|
|
711
|
+
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(() => { });
|
|
712
|
+
// #endregion
|
|
713
|
+
if (isLikelyNoise) {
|
|
714
|
+
setPrompt("");
|
|
715
|
+
setVoiceWaitingForSilence(false);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
setPrompt(text);
|
|
719
|
+
setVoiceWaitingForSilence(true);
|
|
720
|
+
voiceSilenceTimerRef.current = setTimeout(() => {
|
|
721
|
+
// #region agent log
|
|
722
|
+
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(() => { });
|
|
723
|
+
// #endregion
|
|
724
|
+
setVoiceQueuedPrompt(candidate);
|
|
725
|
+
void stopLiveVoice();
|
|
726
|
+
}, VOICE_SILENCE_MS);
|
|
727
|
+
},
|
|
728
|
+
onFinal: (text) => {
|
|
729
|
+
if (voiceSilenceTimerRef.current) {
|
|
730
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
731
|
+
voiceSilenceTimerRef.current = null;
|
|
732
|
+
}
|
|
733
|
+
setPrompt(text);
|
|
734
|
+
liveVoiceRef.current = null;
|
|
735
|
+
setVoicePreparing(false);
|
|
736
|
+
setVoiceListening(false);
|
|
647
737
|
setVoiceWaitingForSilence(false);
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
setPrompt(text);
|
|
651
|
-
setVoiceWaitingForSilence(true);
|
|
652
|
-
voiceSilenceTimerRef.current = setTimeout(() => {
|
|
653
738
|
// #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: '
|
|
739
|
+
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(() => { });
|
|
740
|
+
// #endregion
|
|
741
|
+
},
|
|
742
|
+
onError: (error) => {
|
|
743
|
+
setVoicePreparing(false);
|
|
744
|
+
setVoiceListening(false);
|
|
745
|
+
setVoiceWaitingForSilence(false);
|
|
746
|
+
pushMessage("error", `Voice transcription error: ${error.message}`);
|
|
747
|
+
// #region agent log
|
|
748
|
+
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(() => { });
|
|
655
749
|
// #endregion
|
|
656
|
-
setVoiceQueuedPrompt(candidate);
|
|
657
|
-
void stopLiveVoice();
|
|
658
|
-
}, VOICE_SILENCE_MS);
|
|
659
|
-
},
|
|
660
|
-
onFinal: (text) => {
|
|
661
|
-
if (voiceSilenceTimerRef.current) {
|
|
662
|
-
clearTimeout(voiceSilenceTimerRef.current);
|
|
663
|
-
voiceSilenceTimerRef.current = null;
|
|
664
750
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
setVoiceListening(false);
|
|
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
|
|
673
|
-
},
|
|
674
|
-
onError: (error) => {
|
|
675
|
-
setVoicePreparing(false);
|
|
676
|
-
setVoiceListening(false);
|
|
677
|
-
setVoiceWaitingForSilence(false);
|
|
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
|
|
682
|
-
}
|
|
683
|
-
});
|
|
684
|
-
}, [pushMessage, stopLiveVoice]);
|
|
751
|
+
});
|
|
752
|
+
}); // end resolveAudioDevice.then
|
|
753
|
+
}, [pushMessage, stopLiveVoice, workspaceRoot]);
|
|
685
754
|
useEffect(() => {
|
|
686
755
|
return () => {
|
|
687
756
|
if (voiceSilenceTimerRef.current) {
|
|
@@ -763,6 +832,13 @@ export const AstraApp = () => {
|
|
|
763
832
|
if (key.return) {
|
|
764
833
|
if (trustSelection === 0) {
|
|
765
834
|
trustWorkspace(workspaceRoot);
|
|
835
|
+
// Create .astra settings file at workspace root if it doesn't exist yet.
|
|
836
|
+
try {
|
|
837
|
+
const astraPath = join(workspaceRoot, ".astra");
|
|
838
|
+
if (!existsSync(astraPath))
|
|
839
|
+
writeFileSync(astraPath, "");
|
|
840
|
+
}
|
|
841
|
+
catch { /* non-fatal */ }
|
|
766
842
|
setTrustedWorkspace(true);
|
|
767
843
|
setBooting(true);
|
|
768
844
|
return;
|
|
@@ -776,6 +852,12 @@ export const AstraApp = () => {
|
|
|
776
852
|
exit();
|
|
777
853
|
return;
|
|
778
854
|
}
|
|
855
|
+
if (key.escape && thinking) {
|
|
856
|
+
abortRunRef.current?.abort();
|
|
857
|
+
pushMessage("system", "Cancelled.");
|
|
858
|
+
pushMessage("system", "");
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
779
861
|
if (historyOpen) {
|
|
780
862
|
if (key.escape) {
|
|
781
863
|
if (historyMode === "sessions") {
|
|
@@ -1092,16 +1174,18 @@ export const AstraApp = () => {
|
|
|
1092
1174
|
}
|
|
1093
1175
|
}
|
|
1094
1176
|
if (!isSuperAdmin && event.tool_name === "start_preview") {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1177
|
+
// Server mode returns preview_url (tunnel). Desktop mode returns a
|
|
1178
|
+
// plain message — try to guess the local URL from the command.
|
|
1179
|
+
const tunnelUrl = typeof d.preview_url === "string" ? d.preview_url : null;
|
|
1180
|
+
const command = typeof d.command === "string" ? d.command : "";
|
|
1181
|
+
const localUrl = !tunnelUrl ? guessDevUrl(command) : null;
|
|
1182
|
+
const displayUrl = tunnelUrl ?? localUrl;
|
|
1100
1183
|
pushToolCard({
|
|
1101
1184
|
kind: "preview",
|
|
1102
1185
|
toolName: "start_preview",
|
|
1103
1186
|
locality,
|
|
1104
|
-
summary:
|
|
1187
|
+
summary: "Dev server running",
|
|
1188
|
+
...(displayUrl ? { path: displayUrl } : {}),
|
|
1105
1189
|
});
|
|
1106
1190
|
return null;
|
|
1107
1191
|
}
|
|
@@ -1146,6 +1230,16 @@ export const AstraApp = () => {
|
|
|
1146
1230
|
pushMessage("system", "");
|
|
1147
1231
|
}
|
|
1148
1232
|
}
|
|
1233
|
+
if (event.type === "timing") {
|
|
1234
|
+
const ev = event;
|
|
1235
|
+
if (ev.phase === "llm_done") {
|
|
1236
|
+
const inTok = Number(ev.input_tokens ?? 0);
|
|
1237
|
+
const outTok = Number(ev.output_tokens ?? 0);
|
|
1238
|
+
if (inTok > 0 || outTok > 0)
|
|
1239
|
+
setLoopCtx({ in: inTok, out: outTok });
|
|
1240
|
+
}
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1149
1243
|
if (event.type === "continuation_check") {
|
|
1150
1244
|
const recommendation = typeof event.recommendation === "string" && event.recommendation
|
|
1151
1245
|
? event.recommendation
|
|
@@ -1183,12 +1277,13 @@ export const AstraApp = () => {
|
|
|
1183
1277
|
if (alreadyRepresented) {
|
|
1184
1278
|
return null;
|
|
1185
1279
|
}
|
|
1186
|
-
const summary = summarizeToolResult(toolName, payload, Boolean(event.success));
|
|
1280
|
+
const { summary, path: summaryPath } = summarizeToolResult(toolName, payload, Boolean(event.success));
|
|
1187
1281
|
pushToolCard({
|
|
1188
1282
|
kind: event.success ? "success" : "error",
|
|
1189
1283
|
toolName,
|
|
1190
1284
|
locality: payload.local === true ? "LOCAL" : "REMOTE",
|
|
1191
|
-
summary: event.success ? summary : `${toolName} ${mark}
|
|
1285
|
+
summary: event.success ? summary : `${toolName} ${mark}`,
|
|
1286
|
+
...(summaryPath ? { path: summaryPath } : {}),
|
|
1192
1287
|
});
|
|
1193
1288
|
}
|
|
1194
1289
|
}
|
|
@@ -1200,24 +1295,58 @@ export const AstraApp = () => {
|
|
|
1200
1295
|
}
|
|
1201
1296
|
}
|
|
1202
1297
|
return null;
|
|
1203
|
-
}, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, stopLiveVoice, voiceEnabled, writeLocalFile]);
|
|
1298
|
+
}, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, setLoopCtx, stopLiveVoice, voiceEnabled, writeLocalFile]);
|
|
1204
1299
|
const sendPrompt = useCallback(async (rawPrompt) => {
|
|
1205
1300
|
const text = rawPrompt.trim();
|
|
1206
1301
|
if (!text || !user || thinking) {
|
|
1207
1302
|
return;
|
|
1208
1303
|
}
|
|
1304
|
+
setPrompt("");
|
|
1305
|
+
// Mic onboarding: intercept when waiting for device selection.
|
|
1306
|
+
if (micSetupDevices !== null) {
|
|
1307
|
+
const idx = parseInt(text, 10);
|
|
1308
|
+
const valid = !isNaN(idx) && idx >= 0 && micSetupDevices.some(d => d.index === idx);
|
|
1309
|
+
if (!valid) {
|
|
1310
|
+
pushMessage("error", `Please type one of: ${micSetupDevices.map(d => d.index).join(", ")}`);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const device = `:${idx}`;
|
|
1314
|
+
// Write to .astra local cache
|
|
1315
|
+
writeAstraKey(workspaceRoot, "ASTRA_STT_DEVICE", device);
|
|
1316
|
+
// Persist to backend
|
|
1317
|
+
void backend.updateCliSettings({ audio_device_index: idx });
|
|
1318
|
+
// Update in-process cache
|
|
1319
|
+
setAudioDevice(device);
|
|
1320
|
+
setMicSetupDevices(null);
|
|
1321
|
+
pushMessage("system", `Mic set to [${idx}] ${micSetupDevices.find(d => d.index === idx)?.name ?? ""}. Starting voice...`);
|
|
1322
|
+
pushMessage("system", "");
|
|
1323
|
+
startLiveVoice(false);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1209
1326
|
if (text === "/help") {
|
|
1210
|
-
pushMessage("system",
|
|
1327
|
+
pushMessage("system", isSuperAdmin
|
|
1328
|
+
? "/new /history /voice /dictate on|off|status /tools compact|expanded /settings /settings model <id> /logout /exit"
|
|
1329
|
+
: "/new /history /voice /dictate on|off|status /settings /settings model <id> /logout /exit");
|
|
1211
1330
|
pushMessage("system", "");
|
|
1212
1331
|
return;
|
|
1213
1332
|
}
|
|
1214
1333
|
if (text === "/tools compact") {
|
|
1334
|
+
if (!isSuperAdmin) {
|
|
1335
|
+
pushMessage("system", "Unknown command. Type /help for available commands.");
|
|
1336
|
+
pushMessage("system", "");
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1215
1339
|
setToolFeedMode("compact");
|
|
1216
1340
|
pushMessage("system", "Tool feed set to compact.");
|
|
1217
1341
|
pushMessage("system", "");
|
|
1218
1342
|
return;
|
|
1219
1343
|
}
|
|
1220
1344
|
if (text === "/tools expanded") {
|
|
1345
|
+
if (!isSuperAdmin) {
|
|
1346
|
+
pushMessage("system", "Unknown command. Type /help for available commands.");
|
|
1347
|
+
pushMessage("system", "");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1221
1350
|
setToolFeedMode("expanded");
|
|
1222
1351
|
pushMessage("system", "Tool feed set to expanded.");
|
|
1223
1352
|
pushMessage("system", "");
|
|
@@ -1316,6 +1445,9 @@ export const AstraApp = () => {
|
|
|
1316
1445
|
pushMessage("user", text);
|
|
1317
1446
|
setThinking(true);
|
|
1318
1447
|
setStreamingText("");
|
|
1448
|
+
setLoopCtx(null);
|
|
1449
|
+
const abortController = new AbortController();
|
|
1450
|
+
abortRunRef.current = abortController;
|
|
1319
1451
|
try {
|
|
1320
1452
|
// Scan the local workspace so the backend VirtualFS is populated.
|
|
1321
1453
|
// Merge in any files created/edited during this session so edits
|
|
@@ -1324,8 +1456,25 @@ export const AstraApp = () => {
|
|
|
1324
1456
|
const sessionFiles = Array.from(localFileCache.current.values());
|
|
1325
1457
|
const seenPaths = new Set(sessionFiles.map((f) => f.path));
|
|
1326
1458
|
const mergedFiles = [...sessionFiles, ...scannedFiles.filter((f) => !seenPaths.has(f.path))];
|
|
1327
|
-
|
|
1459
|
+
// `pendingText` is text received since the last committed block.
|
|
1460
|
+
// It gets flushed to the messages list whenever tool activity starts,
|
|
1461
|
+
// keeping text and tool cards in the exact order they were emitted.
|
|
1462
|
+
let pendingText = "";
|
|
1463
|
+
let allAssistantText = "";
|
|
1328
1464
|
let localActionConfirmed = false;
|
|
1465
|
+
const commitPending = (applyGuard = false) => {
|
|
1466
|
+
if (!pendingText.trim()) {
|
|
1467
|
+
pendingText = "";
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const clean = normalizeAssistantText(pendingText);
|
|
1471
|
+
const msg = applyGuard && !localActionConfirmed && looksLikeLocalFilesystemClaim(clean)
|
|
1472
|
+
? `Remote result (not yet confirmed as local filesystem change): ${clean}`
|
|
1473
|
+
: clean;
|
|
1474
|
+
pushMessage("assistant", msg);
|
|
1475
|
+
pendingText = "";
|
|
1476
|
+
setStreamingText("");
|
|
1477
|
+
};
|
|
1329
1478
|
for await (const event of backend.streamChat({
|
|
1330
1479
|
user,
|
|
1331
1480
|
sessionId: activeSessionId,
|
|
@@ -1333,7 +1482,8 @@ export const AstraApp = () => {
|
|
|
1333
1482
|
workspaceRoot,
|
|
1334
1483
|
workspaceTree,
|
|
1335
1484
|
workspaceFiles: mergedFiles,
|
|
1336
|
-
model: activeModel
|
|
1485
|
+
model: activeModel,
|
|
1486
|
+
signal: abortController.signal
|
|
1337
1487
|
})) {
|
|
1338
1488
|
if (event.type === "run_in_terminal") {
|
|
1339
1489
|
localActionConfirmed = true;
|
|
@@ -1347,30 +1497,37 @@ export const AstraApp = () => {
|
|
|
1347
1497
|
if (event.type === "done") {
|
|
1348
1498
|
break;
|
|
1349
1499
|
}
|
|
1500
|
+
// Flush any accumulated text before the first tool event so that text
|
|
1501
|
+
// appears above the tool cards that follow it — preserving order.
|
|
1502
|
+
if (event.type === "tool_start" || event.type === "run_in_terminal") {
|
|
1503
|
+
commitPending();
|
|
1504
|
+
}
|
|
1350
1505
|
const piece = await handleEvent(event, activeSessionId);
|
|
1351
1506
|
if (piece) {
|
|
1352
|
-
|
|
1353
|
-
|
|
1507
|
+
pendingText += piece;
|
|
1508
|
+
allAssistantText += piece;
|
|
1509
|
+
setStreamingText(normalizeAssistantText(pendingText));
|
|
1354
1510
|
}
|
|
1355
1511
|
}
|
|
1356
1512
|
setStreamingText("");
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
: cleanedAssistant;
|
|
1362
|
-
pushMessage("assistant", guardedAssistant);
|
|
1363
|
-
setChatMessages((prev) => [...prev, { role: "assistant", content: cleanedAssistant }]);
|
|
1513
|
+
commitPending(true);
|
|
1514
|
+
// Update conversation history for the backend with the full combined text.
|
|
1515
|
+
if (allAssistantText.trim()) {
|
|
1516
|
+
setChatMessages((prev) => [...prev, { role: "assistant", content: normalizeAssistantText(allAssistantText) }]);
|
|
1364
1517
|
}
|
|
1365
1518
|
else {
|
|
1366
|
-
setChatMessages((prev) => [...prev, { role: "assistant", content:
|
|
1519
|
+
setChatMessages((prev) => [...prev, { role: "assistant", content: allAssistantText }]);
|
|
1367
1520
|
}
|
|
1368
1521
|
pushMessage("system", "");
|
|
1369
1522
|
}
|
|
1370
1523
|
catch (error) {
|
|
1371
|
-
|
|
1524
|
+
// AbortError fires when user cancels — don't show as an error
|
|
1525
|
+
if (error instanceof Error && error.name !== "AbortError") {
|
|
1526
|
+
pushMessage("error", `Error: ${error.message}`);
|
|
1527
|
+
}
|
|
1372
1528
|
}
|
|
1373
1529
|
finally {
|
|
1530
|
+
abortRunRef.current = null;
|
|
1374
1531
|
setThinking(false);
|
|
1375
1532
|
}
|
|
1376
1533
|
}, [
|
|
@@ -1380,9 +1537,11 @@ export const AstraApp = () => {
|
|
|
1380
1537
|
exit,
|
|
1381
1538
|
handleEvent,
|
|
1382
1539
|
localFileCache,
|
|
1540
|
+
micSetupDevices,
|
|
1383
1541
|
openHistory,
|
|
1384
1542
|
pushMessage,
|
|
1385
1543
|
sessionId,
|
|
1544
|
+
setMicSetupDevices,
|
|
1386
1545
|
startLiveVoice,
|
|
1387
1546
|
stopLiveVoice,
|
|
1388
1547
|
thinking,
|
|
@@ -1394,6 +1553,17 @@ export const AstraApp = () => {
|
|
|
1394
1553
|
voiceWaitingForSilence,
|
|
1395
1554
|
workspaceRoot
|
|
1396
1555
|
]);
|
|
1556
|
+
const THINKING_COLORS = ["#6a94f5", "#7aa2ff", "#88b4ff", "#99c4ff", "#aad0ff", "#99c4ff", "#88b4ff", "#7aa2ff"];
|
|
1557
|
+
useEffect(() => {
|
|
1558
|
+
if (!thinking) {
|
|
1559
|
+
setThinkingColorIdx(0);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const interval = setInterval(() => {
|
|
1563
|
+
setThinkingColorIdx((prev) => (prev + 1) % THINKING_COLORS.length);
|
|
1564
|
+
}, 120);
|
|
1565
|
+
return () => clearInterval(interval);
|
|
1566
|
+
}, [thinking]);
|
|
1397
1567
|
useEffect(() => {
|
|
1398
1568
|
if (!voiceQueuedPrompt || !user || thinking) {
|
|
1399
1569
|
return;
|
|
@@ -1456,7 +1626,9 @@ export const AstraApp = () => {
|
|
|
1456
1626
|
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));
|
|
1457
1627
|
}) })), _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 : "--"] })] })] }))] }));
|
|
1458
1628
|
}
|
|
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})` : ""}
|
|
1629
|
+
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})` : ""}${loopCtx
|
|
1630
|
+
? ` · context: ${loopCtx.in >= 1000 ? `${(loopCtx.in / 1000).toFixed(1)}K` : loopCtx.in} / ${loopCtx.out >= 1000 ? `${(loopCtx.out / 1000).toFixed(1)}K` : loopCtx.out}`
|
|
1631
|
+
: ""}` }), _jsx(Text, { color: "#6c88a8", children: `scope ${workspaceRoot.length > HEADER_PATH_MAX ? `…${workspaceRoot.slice(-(HEADER_PATH_MAX - 1))}` : workspaceRoot} · voice ${voiceEnabled
|
|
1460
1632
|
? voicePreparing
|
|
1461
1633
|
? "on/preparing"
|
|
1462
1634
|
: voiceListening
|
|
@@ -1464,7 +1636,7 @@ export const AstraApp = () => {
|
|
|
1464
1636
|
? "on/waiting"
|
|
1465
1637
|
: "on/listening"
|
|
1466
1638
|
: "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) => {
|
|
1639
|
+
: "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: isSuperAdmin ? "/tools compact|expanded /settings /logout /exit" : "/settings /logout /exit" }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [messages.map((message, index) => {
|
|
1468
1640
|
const prev = index > 0 ? messages[index - 1] : null;
|
|
1469
1641
|
const style = styleForKind(message.kind);
|
|
1470
1642
|
const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
|
|
@@ -1474,11 +1646,21 @@ export const AstraApp = () => {
|
|
|
1474
1646
|
message.kind !== "system" &&
|
|
1475
1647
|
prev?.kind !== message.kind &&
|
|
1476
1648
|
(message.kind === "assistant" || message.kind === "tool");
|
|
1649
|
+
// Show a subtle turn separator before each assistant response that
|
|
1650
|
+
// follows a tool block — makes it easy to see where one turn ends.
|
|
1651
|
+
const needsTurnSeparator = message.kind === "assistant" &&
|
|
1652
|
+
Boolean(prev) &&
|
|
1653
|
+
prev?.kind === "tool";
|
|
1477
1654
|
if (isSpacing) {
|
|
1478
1655
|
return _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: " " }) }, `${index}-spacer`);
|
|
1479
1656
|
}
|
|
1480
1657
|
if (message.kind === "tool" && message.card && !isSuperAdmin) {
|
|
1481
1658
|
const card = message.card;
|
|
1659
|
+
// In compact mode, hide "start" spinner cards — they create noise
|
|
1660
|
+
// (one per tool call) without adding signal after the run completes.
|
|
1661
|
+
if (toolFeedMode === "compact" && card.kind === "start") {
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1482
1664
|
const localityLabel = card.locality === "LOCAL" ? "local" : "cloud";
|
|
1483
1665
|
const icon = card.kind === "error"
|
|
1484
1666
|
? "✕"
|
|
@@ -1506,11 +1688,16 @@ export const AstraApp = () => {
|
|
|
1506
1688
|
: card.kind === "preview"
|
|
1507
1689
|
? "#9ad5ff"
|
|
1508
1690
|
: "#9bc5ff";
|
|
1509
|
-
|
|
1691
|
+
// Timestamps fade with age: bright for recent, dim for older
|
|
1692
|
+
const tsAge = message.ts ? Date.now() - message.ts : Infinity;
|
|
1693
|
+
const tsColor = tsAge < 120000 ? "#68d5c8" : tsAge < 600000 ? "#3d8a82" : "#2a5a55";
|
|
1694
|
+
return (_jsxs(React.Fragment, { children: [needsTurnSeparator ? _jsx(Text, { color: "#1e2e40", children: "─".repeat(48) }) : needsGroupGap ? _jsx(Text, { children: " " }) : null, _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, children: paddedLabel }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Text, { color: accent, children: [icon, " ", card.summary, card.path ? _jsxs(Text, { color: "#ffffff", children: [" ", card.path] }) : null, 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] }), message.ts ? _jsxs(Text, { color: tsColor, children: [" ", formatTime(message.ts)] }) : null] })] })] }, `${index}-${message.kind}`));
|
|
1510
1695
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1696
|
+
const showTs = Boolean(message.ts) && (message.kind === "user" || message.kind === "assistant");
|
|
1697
|
+
const tsAge2 = message.ts ? Date.now() - message.ts : Infinity;
|
|
1698
|
+
const tsColor2 = tsAge2 < 120000 ? "#68d5c8" : tsAge2 < 600000 ? "#3d8a82" : "#2a5a55";
|
|
1699
|
+
return (_jsxs(React.Fragment, { children: [needsTurnSeparator ? _jsx(Text, { color: "#1e2e40", children: "─".repeat(48) }) : needsGroupGap ? _jsx(Text, { children: " " }) : null, _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, bold: style.bold, children: paddedLabel }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [message.kind === "assistant" ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", children: renderMarkdownContent(message.text, style.textColor, `assistant-${index}`) })) : (_jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: style.textColor, bold: style.bold && message.kind === "error", children: message.text }) })), showTs ? _jsxs(Text, { color: tsColor2, children: [" ", formatTime(message.ts)] }) : null] })] })] }, `${index}-${message.kind}`));
|
|
1700
|
+
}), 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 && !streamingText ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#7aa2ff", children: "◆ astra".padEnd(LABEL_WIDTH, " ") }), _jsxs(Text, { color: (THINKING_COLORS[thinkingColorIdx] ?? "#7aa2ff"), children: [_jsx(Spinner, { type: "dots2" }), _jsx(Text, { color: (THINKING_COLORS[thinkingColorIdx] ?? "#7aa2ff"), children: " Working..." }), _jsx(Text, { color: "#2a4060", children: " esc to cancel" })] })] })) : 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) => {
|
|
1514
1701
|
void sendPrompt(value);
|
|
1515
1702
|
}, onChange: (value) => {
|
|
1516
1703
|
if (!voiceListening) {
|