@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 +8 -0
- package/dist/app/App.js +256 -37
- package/dist/app/App.js.map +1 -1
- package/dist/lib/backendClient.js +5 -1
- package/dist/lib/backendClient.js.map +1 -1
- package/dist/lib/voice.js +19 -2
- package/dist/lib/voice.js.map +1 -1
- package/package.json +1 -1
- package/src/app/App.tsx +338 -111
- package/src/lib/backendClient.ts +5 -1
- package/src/lib/voice.ts +19 -2
- package/src/types/events.ts +11 -0
package/src/app/App.tsx
CHANGED
|
@@ -34,6 +34,7 @@ type ToolCard = {
|
|
|
34
34
|
toolName: string;
|
|
35
35
|
locality: "LOCAL" | "REMOTE";
|
|
36
36
|
summary: string;
|
|
37
|
+
count?: number;
|
|
37
38
|
path?: string;
|
|
38
39
|
snippetLines?: string[];
|
|
39
40
|
};
|
|
@@ -58,6 +59,7 @@ const ASTRA_ASCII = `
|
|
|
58
59
|
### ### ######## ### ### ### ### ### ######## ######## ######### ##########
|
|
59
60
|
by Sean Donovan
|
|
60
61
|
`;
|
|
62
|
+
const ASTRA_COMPACT = "ASTRA CODE";
|
|
61
63
|
|
|
62
64
|
const WELCOME_WIDTH = 96;
|
|
63
65
|
|
|
@@ -74,6 +76,10 @@ const FOUNDER_WELCOME = centerLine("Welcome to Astra from Astra CEO & Founder, S
|
|
|
74
76
|
|
|
75
77
|
const HISTORY_SETTINGS_URL = "https://astra-web-builder.vercel.app/settings";
|
|
76
78
|
const VOICE_SILENCE_MS = Number(process.env.ASTRA_VOICE_SILENCE_MS ?? "3000");
|
|
79
|
+
|
|
80
|
+
const VOICE_MIN_CHARS = Number(process.env.ASTRA_VOICE_MIN_CHARS ?? "4");
|
|
81
|
+
const VOICE_DUPLICATE_WINDOW_MS = Number(process.env.ASTRA_VOICE_DUPLICATE_WINDOW_MS ?? "10000");
|
|
82
|
+
const VOICE_NOISE_WORDS = new Set(["you", "yes", "yeah", "yep", "ok", "okay", "uh", "um", "hmm"]);
|
|
77
83
|
const TOOL_SNIPPET_LINES = 6;
|
|
78
84
|
const NOISY_EVENT_TYPES = new Set([
|
|
79
85
|
"timing",
|
|
@@ -190,10 +196,10 @@ const extractAssistantText = (event: AgentEvent): string | null => {
|
|
|
190
196
|
return null;
|
|
191
197
|
};
|
|
192
198
|
|
|
193
|
-
const DIVIDER =
|
|
194
|
-
"────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────";
|
|
195
|
-
|
|
196
199
|
const LABEL_WIDTH = 10;
|
|
200
|
+
const HEADER_PATH_MAX = Number(process.env.ASTRA_HEADER_PATH_MAX ?? "42");
|
|
201
|
+
const MIN_DIVIDER = 64;
|
|
202
|
+
const MAX_DIVIDER = 120;
|
|
197
203
|
|
|
198
204
|
type MessageStyle = {
|
|
199
205
|
label: string;
|
|
@@ -238,14 +244,86 @@ const normalizeAssistantText = (input: string): string => {
|
|
|
238
244
|
return "";
|
|
239
245
|
}
|
|
240
246
|
|
|
241
|
-
|
|
247
|
+
let out = input
|
|
242
248
|
// Remove control chars but preserve newlines/tabs.
|
|
243
249
|
.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, "")
|
|
250
|
+
.replace(/([a-z0-9/])([A-Z])/g, "$1 $2")
|
|
251
|
+
.replace(/([.!?])([A-Za-z])/g, "$1 $2")
|
|
252
|
+
.replace(/([a-z])(\u2022)/g, "$1\n$2")
|
|
253
|
+
.replace(/([^\n])(Summary:|Next:|Tests:)/g, "$1\n\n$2")
|
|
254
|
+
.replace(/([A-Za-z0-9_/-]+)\.\s+md\b/g, "$1.md")
|
|
255
|
+
.replace(/(\bnpm audit)(All tasks complete\.)/gi, "$1\n\n$2")
|
|
256
|
+
.replace(/([.!?])\s*(Summary:|Next:|Tests:)/g, "$1\n\n$2")
|
|
257
|
+
.replace(/([^\n])(\u2022\s)/g, "$1\n$2")
|
|
244
258
|
// Trim trailing spaces line-by-line.
|
|
245
259
|
.replace(/[ \t]+$/gm, "")
|
|
246
260
|
// Normalize excessive blank lines.
|
|
247
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
248
|
-
|
|
261
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
262
|
+
|
|
263
|
+
const paragraphs = out
|
|
264
|
+
.split(/\n{2,}/)
|
|
265
|
+
.map((p) => p.trim())
|
|
266
|
+
.filter(Boolean);
|
|
267
|
+
const seen = new Set<string>();
|
|
268
|
+
const deduped: string[] = [];
|
|
269
|
+
for (const para of paragraphs) {
|
|
270
|
+
const key = para.toLowerCase().replace(/\s+/g, " ");
|
|
271
|
+
if (para.length > 50 && seen.has(key)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
seen.add(key);
|
|
275
|
+
deduped.push(para);
|
|
276
|
+
}
|
|
277
|
+
return deduped.join("\n\n").trim();
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const summarizeToolResult = (toolName: string, data: Record<string, unknown>, success: boolean): string => {
|
|
281
|
+
if (!success) {
|
|
282
|
+
return `${toolName} failed`;
|
|
283
|
+
}
|
|
284
|
+
const path = typeof data.path === "string" ? data.path : "";
|
|
285
|
+
const totalLines = typeof data.total_lines === "number" ? data.total_lines : null;
|
|
286
|
+
if (toolName === "view_file") {
|
|
287
|
+
if (totalLines !== null && path) {
|
|
288
|
+
return `Read ${totalLines} lines of <${path}>`;
|
|
289
|
+
}
|
|
290
|
+
if (path) {
|
|
291
|
+
return `Read <${path}>`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (toolName === "list_directory") {
|
|
295
|
+
const dir = path || ".";
|
|
296
|
+
if (totalLines !== null) {
|
|
297
|
+
return `Listed ${totalLines} entries in <${dir}>`;
|
|
298
|
+
}
|
|
299
|
+
return `Listed <${dir}>`;
|
|
300
|
+
}
|
|
301
|
+
if (toolName === "semantic_search") {
|
|
302
|
+
return "Searched codebase context";
|
|
303
|
+
}
|
|
304
|
+
if (toolName === "search_files") {
|
|
305
|
+
return totalLines !== null ? `Found ${totalLines} matching paths` : "Searched file paths";
|
|
306
|
+
}
|
|
307
|
+
return `${toolName} completed`;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const isLikelyVoiceNoise = (text: string): boolean => {
|
|
311
|
+
const normalized = text.trim().toLowerCase();
|
|
312
|
+
if (!normalized) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (normalized.length < VOICE_MIN_CHARS) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
const tokens = normalized.split(/\s+/).filter(Boolean);
|
|
319
|
+
if (tokens.length === 0) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
const nonNoise = tokens.filter((t) => !VOICE_NOISE_WORDS.has(t));
|
|
323
|
+
if (nonNoise.length === 0) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
249
327
|
};
|
|
250
328
|
|
|
251
329
|
type InlineToken = {text: string; bold?: boolean; italic?: boolean; code?: boolean};
|
|
@@ -507,6 +585,20 @@ const renderMarkdownContent = (text: string, baseColor: string, keyPrefix: strin
|
|
|
507
585
|
|
|
508
586
|
export const AstraApp = (): React.JSX.Element => {
|
|
509
587
|
const workspaceRoot = useMemo(() => process.cwd(), []);
|
|
588
|
+
const [terminalWidth, setTerminalWidth] = useState<number>(() => process.stdout.columns || MAX_DIVIDER);
|
|
589
|
+
useEffect(() => {
|
|
590
|
+
const updateWidth = (): void => {
|
|
591
|
+
setTerminalWidth(process.stdout.columns || MAX_DIVIDER);
|
|
592
|
+
};
|
|
593
|
+
process.stdout.on("resize", updateWidth);
|
|
594
|
+
return () => {
|
|
595
|
+
process.stdout.off("resize", updateWidth);
|
|
596
|
+
};
|
|
597
|
+
}, []);
|
|
598
|
+
const dividerWidth = Math.max(MIN_DIVIDER, Math.min(MAX_DIVIDER, (terminalWidth ?? MAX_DIVIDER) - 2));
|
|
599
|
+
const divider = "─".repeat(dividerWidth);
|
|
600
|
+
const brand = dividerWidth < 96 ? ASTRA_COMPACT : ASTRA_ASCII;
|
|
601
|
+
const welcomeLine = dividerWidth < 96 ? "Welcome to Astra" : FOUNDER_WELCOME;
|
|
510
602
|
const backend = useMemo(() => new BackendClient(), []);
|
|
511
603
|
const {exit} = useApp();
|
|
512
604
|
|
|
@@ -565,8 +657,10 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
565
657
|
const [streamingText, setStreamingText] = useState("");
|
|
566
658
|
const [voiceEnabled, setVoiceEnabled] = useState(false);
|
|
567
659
|
const [voiceListening, setVoiceListening] = useState(false);
|
|
660
|
+
const [voicePreparing, setVoicePreparing] = useState(false);
|
|
568
661
|
const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
|
|
569
662
|
const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState<string | null>(null);
|
|
663
|
+
const [toolFeedMode, setToolFeedMode] = useState<"compact" | "expanded">("compact");
|
|
570
664
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
571
665
|
const [historyMode, setHistoryMode] = useState<HistoryMode>("picker");
|
|
572
666
|
const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
|
|
@@ -576,6 +670,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
576
670
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
577
671
|
const liveVoiceRef = useRef<LiveTranscriptionController | null>(null);
|
|
578
672
|
const voiceSilenceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
673
|
+
const lastVoicePromptRef = useRef<{text: string; at: number} | null>(null);
|
|
674
|
+
const lastIgnoredVoiceRef = useRef<{text: string; at: number} | null>(null);
|
|
579
675
|
const fileEditBuffersRef = useRef<Map<string, {path: string; chunks: string[]; toolName: string | undefined}>>(new Map());
|
|
580
676
|
const isSuperAdmin = user?.role === "super_admin";
|
|
581
677
|
|
|
@@ -584,7 +680,28 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
584
680
|
}, []);
|
|
585
681
|
|
|
586
682
|
const pushToolCard = useCallback((card: ToolCard) => {
|
|
587
|
-
setMessages((prev) =>
|
|
683
|
+
setMessages((prev) => {
|
|
684
|
+
const nextEntry = {kind: "tool", text: card.summary, card} satisfies UiMessage;
|
|
685
|
+
const last = prev[prev.length - 1];
|
|
686
|
+
if (
|
|
687
|
+
last &&
|
|
688
|
+
last.kind === "tool" &&
|
|
689
|
+
last.card &&
|
|
690
|
+
last.card.toolName === card.toolName &&
|
|
691
|
+
last.card.kind === card.kind &&
|
|
692
|
+
last.card.summary === card.summary &&
|
|
693
|
+
last.card.locality === card.locality
|
|
694
|
+
) {
|
|
695
|
+
const updated = [...prev];
|
|
696
|
+
const existingCount = Math.max(1, Number(last.card.count ?? 1));
|
|
697
|
+
updated[updated.length - 1] = {
|
|
698
|
+
...last,
|
|
699
|
+
card: {...last.card, count: existingCount + 1}
|
|
700
|
+
};
|
|
701
|
+
return updated.slice(-300);
|
|
702
|
+
}
|
|
703
|
+
return [...prev, nextEntry].slice(-300);
|
|
704
|
+
});
|
|
588
705
|
}, []);
|
|
589
706
|
|
|
590
707
|
const filteredHistory = useMemo(() => {
|
|
@@ -659,11 +776,13 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
659
776
|
}
|
|
660
777
|
const controller = liveVoiceRef.current;
|
|
661
778
|
if (!controller) {
|
|
779
|
+
setVoicePreparing(false);
|
|
662
780
|
setVoiceWaitingForSilence(false);
|
|
663
781
|
return;
|
|
664
782
|
}
|
|
665
783
|
liveVoiceRef.current = null;
|
|
666
784
|
await controller.stop();
|
|
785
|
+
setVoicePreparing(false);
|
|
667
786
|
setVoiceListening(false);
|
|
668
787
|
setVoiceWaitingForSilence(false);
|
|
669
788
|
}, []);
|
|
@@ -672,24 +791,45 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
672
791
|
if (liveVoiceRef.current) {
|
|
673
792
|
return;
|
|
674
793
|
}
|
|
794
|
+
// #region agent log
|
|
795
|
+
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(()=>{});
|
|
796
|
+
// #endregion
|
|
675
797
|
setVoiceEnabled(true);
|
|
676
|
-
|
|
798
|
+
setVoicePreparing(true);
|
|
799
|
+
setVoiceListening(false);
|
|
677
800
|
setVoiceWaitingForSilence(false);
|
|
678
801
|
if (announce) {
|
|
679
|
-
pushMessage("system", "Voice input
|
|
802
|
+
pushMessage("system", "Voice input armed. Preparing microphone...");
|
|
680
803
|
}
|
|
681
804
|
liveVoiceRef.current = startLiveTranscription({
|
|
682
805
|
onPartial: (text) => {
|
|
683
|
-
|
|
806
|
+
setVoicePreparing(false);
|
|
807
|
+
setVoiceListening(true);
|
|
684
808
|
if (voiceSilenceTimerRef.current) {
|
|
685
809
|
clearTimeout(voiceSilenceTimerRef.current);
|
|
686
810
|
}
|
|
687
811
|
const candidate = text.trim();
|
|
688
812
|
if (!candidate) {
|
|
813
|
+
setPrompt("");
|
|
814
|
+
setVoiceWaitingForSilence(false);
|
|
689
815
|
return;
|
|
690
816
|
}
|
|
817
|
+
const normalized = candidate.toLowerCase();
|
|
818
|
+
const isLikelyNoise = isLikelyVoiceNoise(normalized);
|
|
819
|
+
// #region agent log
|
|
820
|
+
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(()=>{});
|
|
821
|
+
// #endregion
|
|
822
|
+
if (isLikelyNoise) {
|
|
823
|
+
setPrompt("");
|
|
824
|
+
setVoiceWaitingForSilence(false);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
setPrompt(text);
|
|
691
828
|
setVoiceWaitingForSilence(true);
|
|
692
829
|
voiceSilenceTimerRef.current = setTimeout(() => {
|
|
830
|
+
// #region agent log
|
|
831
|
+
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(()=>{});
|
|
832
|
+
// #endregion
|
|
693
833
|
setVoiceQueuedPrompt(candidate);
|
|
694
834
|
void stopLiveVoice();
|
|
695
835
|
}, VOICE_SILENCE_MS);
|
|
@@ -701,12 +841,21 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
701
841
|
}
|
|
702
842
|
setPrompt(text);
|
|
703
843
|
liveVoiceRef.current = null;
|
|
844
|
+
setVoicePreparing(false);
|
|
704
845
|
setVoiceListening(false);
|
|
705
846
|
setVoiceWaitingForSilence(false);
|
|
847
|
+
// #region agent log
|
|
848
|
+
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(()=>{});
|
|
849
|
+
// #endregion
|
|
706
850
|
},
|
|
707
851
|
onError: (error) => {
|
|
852
|
+
setVoicePreparing(false);
|
|
853
|
+
setVoiceListening(false);
|
|
708
854
|
setVoiceWaitingForSilence(false);
|
|
709
855
|
pushMessage("error", `Voice transcription error: ${error.message}`);
|
|
856
|
+
// #region agent log
|
|
857
|
+
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(()=>{});
|
|
858
|
+
// #endregion
|
|
710
859
|
}
|
|
711
860
|
});
|
|
712
861
|
}, [pushMessage, stopLiveVoice]);
|
|
@@ -1197,6 +1346,26 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1197
1346
|
|
|
1198
1347
|
if (event.type === "credits_exhausted") {
|
|
1199
1348
|
setCreditsRemaining(0);
|
|
1349
|
+
if (voiceEnabled) {
|
|
1350
|
+
await stopLiveVoice();
|
|
1351
|
+
setVoiceEnabled(false);
|
|
1352
|
+
pushMessage("system", "Voice input paused: credits exhausted. Recharge, then run /voice on.");
|
|
1353
|
+
pushMessage("system", "");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (event.type === "continuation_check") {
|
|
1358
|
+
const recommendation =
|
|
1359
|
+
typeof event.recommendation === "string" && event.recommendation
|
|
1360
|
+
? event.recommendation
|
|
1361
|
+
: "Please narrow the scope and continue with a specific target.";
|
|
1362
|
+
const streak = Number(event.consecutive_read_only_iterations ?? 0);
|
|
1363
|
+
const threshold = Number(event.threshold ?? 0);
|
|
1364
|
+
pushMessage(
|
|
1365
|
+
"system",
|
|
1366
|
+
`Exploration paused (${streak}/${threshold} read-only turns). ${recommendation}`
|
|
1367
|
+
);
|
|
1368
|
+
return null;
|
|
1200
1369
|
}
|
|
1201
1370
|
|
|
1202
1371
|
if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
|
|
@@ -1204,14 +1373,6 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1204
1373
|
}
|
|
1205
1374
|
|
|
1206
1375
|
if (!isSuperAdmin && event.type === "tool_start") {
|
|
1207
|
-
const tool = event.tool as {name?: string} | undefined;
|
|
1208
|
-
const name = tool?.name ?? "tool";
|
|
1209
|
-
pushToolCard({
|
|
1210
|
-
kind: "start",
|
|
1211
|
-
toolName: name,
|
|
1212
|
-
locality: "REMOTE",
|
|
1213
|
-
summary: `${name} is running...`
|
|
1214
|
-
});
|
|
1215
1376
|
return null;
|
|
1216
1377
|
}
|
|
1217
1378
|
|
|
@@ -1225,11 +1386,23 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1225
1386
|
} else if (event.type === "tool_result") {
|
|
1226
1387
|
const mark = event.success ? "completed" : "failed";
|
|
1227
1388
|
const toolName = typeof event.tool_name === "string" ? event.tool_name : "tool";
|
|
1389
|
+
const payload = (event.data ?? {}) as Record<string, unknown>;
|
|
1390
|
+
const resultType = typeof event.result_type === "string" ? event.result_type : "";
|
|
1391
|
+
const alreadyRepresented =
|
|
1392
|
+
resultType === "file_create" ||
|
|
1393
|
+
resultType === "file_edit" ||
|
|
1394
|
+
resultType === "file_delete" ||
|
|
1395
|
+
toolName === "start_preview" ||
|
|
1396
|
+
toolName === "capture_screenshot";
|
|
1397
|
+
if (alreadyRepresented) {
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
const summary = summarizeToolResult(toolName, payload, Boolean(event.success));
|
|
1228
1401
|
pushToolCard({
|
|
1229
1402
|
kind: event.success ? "success" : "error",
|
|
1230
1403
|
toolName,
|
|
1231
|
-
locality:
|
|
1232
|
-
summary: `${toolName} ${mark}`
|
|
1404
|
+
locality: payload.local === true ? "LOCAL" : "REMOTE",
|
|
1405
|
+
summary: event.success ? summary : `${toolName} ${mark}`
|
|
1233
1406
|
});
|
|
1234
1407
|
}
|
|
1235
1408
|
}
|
|
@@ -1241,7 +1414,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1241
1414
|
}
|
|
1242
1415
|
return null;
|
|
1243
1416
|
},
|
|
1244
|
-
[backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]
|
|
1417
|
+
[backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, stopLiveVoice, voiceEnabled, writeLocalFile]
|
|
1245
1418
|
);
|
|
1246
1419
|
|
|
1247
1420
|
const sendPrompt = useCallback(
|
|
@@ -1254,15 +1427,27 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1254
1427
|
if (text === "/help") {
|
|
1255
1428
|
pushMessage(
|
|
1256
1429
|
"system",
|
|
1257
|
-
"/new /history /voice /
|
|
1430
|
+
"/new /history /voice /dictate on|off|status /tools compact|expanded /settings /settings model <id> /logout /exit"
|
|
1258
1431
|
);
|
|
1259
1432
|
pushMessage("system", "");
|
|
1260
1433
|
return;
|
|
1261
1434
|
}
|
|
1435
|
+
if (text === "/tools compact") {
|
|
1436
|
+
setToolFeedMode("compact");
|
|
1437
|
+
pushMessage("system", "Tool feed set to compact.");
|
|
1438
|
+
pushMessage("system", "");
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (text === "/tools expanded") {
|
|
1442
|
+
setToolFeedMode("expanded");
|
|
1443
|
+
pushMessage("system", "Tool feed set to expanded.");
|
|
1444
|
+
pushMessage("system", "");
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1262
1447
|
if (text === "/settings") {
|
|
1263
1448
|
pushMessage(
|
|
1264
1449
|
"system",
|
|
1265
|
-
`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()}`
|
|
1450
|
+
`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()}`
|
|
1266
1451
|
);
|
|
1267
1452
|
pushMessage("system", "");
|
|
1268
1453
|
return;
|
|
@@ -1300,34 +1485,34 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1300
1485
|
await openHistory();
|
|
1301
1486
|
return;
|
|
1302
1487
|
}
|
|
1303
|
-
if (text === "/
|
|
1488
|
+
if (text === "/dictate status") {
|
|
1304
1489
|
pushMessage(
|
|
1305
1490
|
"system",
|
|
1306
|
-
`Voice input is ${voiceEnabled ? "on" : "off"}${
|
|
1491
|
+
`Voice input is ${voiceEnabled ? "on" : "off"}${voicePreparing ? " (preparing mic)" : ""}${voiceListening ? " (listening)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`
|
|
1307
1492
|
);
|
|
1308
1493
|
pushMessage("system", "");
|
|
1309
1494
|
return;
|
|
1310
1495
|
}
|
|
1311
|
-
if (text === "/
|
|
1496
|
+
if (text === "/dictate on") {
|
|
1312
1497
|
setVoiceEnabled(true);
|
|
1313
1498
|
startLiveVoice(true);
|
|
1314
1499
|
pushMessage("system", `Voice input enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
|
|
1315
1500
|
pushMessage("system", "");
|
|
1316
1501
|
return;
|
|
1317
1502
|
}
|
|
1318
|
-
if (text === "/
|
|
1503
|
+
if (text === "/dictate off") {
|
|
1319
1504
|
await stopLiveVoice();
|
|
1320
1505
|
setVoiceEnabled(false);
|
|
1321
1506
|
pushMessage("system", "Voice input disabled.");
|
|
1322
1507
|
pushMessage("system", "");
|
|
1323
1508
|
return;
|
|
1324
1509
|
}
|
|
1325
|
-
if (text === "/
|
|
1510
|
+
if (text === "/dictate input") {
|
|
1326
1511
|
const transcribed = await transcribeOnce();
|
|
1327
1512
|
if (!transcribed) {
|
|
1328
1513
|
pushMessage(
|
|
1329
1514
|
"error",
|
|
1330
|
-
"No speech transcribed. Ensure you're signed in and
|
|
1515
|
+
"No speech transcribed. Ensure you're signed in and your microphone capture works (optional ASTRA_STT_CAPTURE_COMMAND)."
|
|
1331
1516
|
);
|
|
1332
1517
|
return;
|
|
1333
1518
|
}
|
|
@@ -1399,6 +1584,9 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1399
1584
|
localActionConfirmed = true;
|
|
1400
1585
|
}
|
|
1401
1586
|
}
|
|
1587
|
+
if (event.type === "done") {
|
|
1588
|
+
break;
|
|
1589
|
+
}
|
|
1402
1590
|
const piece = await handleEvent(event, activeSessionId);
|
|
1403
1591
|
if (piece) {
|
|
1404
1592
|
assistant += piece;
|
|
@@ -1438,9 +1626,11 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1438
1626
|
startLiveVoice,
|
|
1439
1627
|
stopLiveVoice,
|
|
1440
1628
|
thinking,
|
|
1629
|
+
toolFeedMode,
|
|
1441
1630
|
user,
|
|
1442
1631
|
voiceEnabled,
|
|
1443
1632
|
voiceListening,
|
|
1633
|
+
voicePreparing,
|
|
1444
1634
|
voiceWaitingForSilence,
|
|
1445
1635
|
workspaceRoot
|
|
1446
1636
|
]
|
|
@@ -1455,8 +1645,35 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1455
1645
|
if (!queued) {
|
|
1456
1646
|
return;
|
|
1457
1647
|
}
|
|
1648
|
+
const normalizedQueued = queued.toLowerCase();
|
|
1649
|
+
const last = lastVoicePromptRef.current;
|
|
1650
|
+
const isLikelyNoise = isLikelyVoiceNoise(normalizedQueued);
|
|
1651
|
+
const isFastDuplicate =
|
|
1652
|
+
last !== null &&
|
|
1653
|
+
last.text === normalizedQueued &&
|
|
1654
|
+
Date.now() - last.at <= VOICE_DUPLICATE_WINDOW_MS;
|
|
1655
|
+
// #region agent log
|
|
1656
|
+
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(()=>{});
|
|
1657
|
+
// #endregion
|
|
1658
|
+
if (isLikelyNoise || isFastDuplicate) {
|
|
1659
|
+
const now = Date.now();
|
|
1660
|
+
const lastIgnored = lastIgnoredVoiceRef.current;
|
|
1661
|
+
const shouldLogIgnored =
|
|
1662
|
+
!lastIgnored ||
|
|
1663
|
+
lastIgnored.text !== normalizedQueued ||
|
|
1664
|
+
now - lastIgnored.at > VOICE_DUPLICATE_WINDOW_MS;
|
|
1665
|
+
if (shouldLogIgnored) {
|
|
1666
|
+
pushMessage("system", `Ignored likely-noise dictation input: "${queued}"`);
|
|
1667
|
+
lastIgnoredVoiceRef.current = {text: normalizedQueued, at: now};
|
|
1668
|
+
}
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
lastVoicePromptRef.current = {text: normalizedQueued, at: Date.now()};
|
|
1458
1672
|
pushMessage("system", `Voice input: ${queued}`);
|
|
1459
1673
|
setPrompt("");
|
|
1674
|
+
// #region agent log
|
|
1675
|
+
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(()=>{});
|
|
1676
|
+
// #endregion
|
|
1460
1677
|
void sendPrompt(queued);
|
|
1461
1678
|
}, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
|
|
1462
1679
|
|
|
@@ -1500,8 +1717,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1500
1717
|
if (booting) {
|
|
1501
1718
|
return (
|
|
1502
1719
|
<Box flexDirection="column">
|
|
1503
|
-
<Text color="#7aa2ff">{
|
|
1504
|
-
<Text color="#8ea1bd">{
|
|
1720
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1721
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1505
1722
|
<Text color="#8aa2c9">
|
|
1506
1723
|
<Spinner type="dots12" /> Booting Astra terminal shell...
|
|
1507
1724
|
</Text>
|
|
@@ -1512,8 +1729,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1512
1729
|
if (bootError) {
|
|
1513
1730
|
return (
|
|
1514
1731
|
<Box flexDirection="column">
|
|
1515
|
-
<Text color="#7aa2ff">{
|
|
1516
|
-
<Text color="#8ea1bd">{
|
|
1732
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1733
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1517
1734
|
<Text color="red">{bootError}</Text>
|
|
1518
1735
|
</Box>
|
|
1519
1736
|
);
|
|
@@ -1522,8 +1739,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1522
1739
|
if (!user) {
|
|
1523
1740
|
return (
|
|
1524
1741
|
<Box flexDirection="column">
|
|
1525
|
-
<Text color="#7aa2ff">{
|
|
1526
|
-
<Text color="#8ea1bd">{
|
|
1742
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1743
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1527
1744
|
<Text color="#b8c8ff">
|
|
1528
1745
|
Astra terminal AI pair programmer ({loginMode === "login" ? "Sign in" : "Create account"})
|
|
1529
1746
|
</Text>
|
|
@@ -1567,16 +1784,16 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1567
1784
|
const selected = filteredHistory[historyIndex];
|
|
1568
1785
|
return (
|
|
1569
1786
|
<Box flexDirection="column">
|
|
1570
|
-
<Text color="#7aa2ff">{
|
|
1571
|
-
<Text color="#8ea1bd">{
|
|
1572
|
-
<Text color="#2a3a50">{
|
|
1787
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1788
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1789
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1573
1790
|
{historyMode === "picker" ? (
|
|
1574
1791
|
<Box flexDirection="column">
|
|
1575
1792
|
<Box flexDirection="row" justifyContent="space-between">
|
|
1576
1793
|
<Text color="#dce9ff">History Picker</Text>
|
|
1577
1794
|
<Text color="#5a7a9a">Esc close · Enter select</Text>
|
|
1578
1795
|
</Box>
|
|
1579
|
-
<Text color="#2a3a50">{
|
|
1796
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1580
1797
|
<Box marginTop={1} flexDirection="column">
|
|
1581
1798
|
<Box flexDirection="row">
|
|
1582
1799
|
<Text color={historyPickerIndex === 0 ? "#dce9ff" : "#7a9bba"}>
|
|
@@ -1589,7 +1806,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1589
1806
|
</Text>
|
|
1590
1807
|
</Box>
|
|
1591
1808
|
</Box>
|
|
1592
|
-
<Text color="#2a3a50">{
|
|
1809
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1593
1810
|
<Text color="#5a7a9a">Credit usage history opens: {HISTORY_SETTINGS_URL}</Text>
|
|
1594
1811
|
</Box>
|
|
1595
1812
|
) : (
|
|
@@ -1602,7 +1819,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1602
1819
|
<Text color="#4a6070">search </Text>
|
|
1603
1820
|
<TextInput value={historyQuery} onChange={setHistoryQuery} placeholder="Filter chats..." />
|
|
1604
1821
|
</Box>
|
|
1605
|
-
<Text color="#2a3a50">{
|
|
1822
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1606
1823
|
{historyLoading ? (
|
|
1607
1824
|
<Text color="#8aa2c9">
|
|
1608
1825
|
<Spinner type="dots12" /> Loading chat history...
|
|
@@ -1628,7 +1845,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1628
1845
|
})}
|
|
1629
1846
|
</Box>
|
|
1630
1847
|
)}
|
|
1631
|
-
<Text color="#2a3a50">{
|
|
1848
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1632
1849
|
<Box flexDirection="row" justifyContent="space-between">
|
|
1633
1850
|
<Text color="#5a7a9a">
|
|
1634
1851
|
Page {historyPage + 1} / {historyPageCount}
|
|
@@ -1643,57 +1860,53 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1643
1860
|
|
|
1644
1861
|
return (
|
|
1645
1862
|
<Box flexDirection="column">
|
|
1646
|
-
<Text color="#7aa2ff">{
|
|
1647
|
-
<Text color="#8ea1bd">{
|
|
1863
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1864
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1648
1865
|
{/*<Text color="#9aa8c1">Astra Code · {process.cwd()}</Text>*/}
|
|
1649
|
-
<Text color="#2a3a50">{
|
|
1650
|
-
<
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
<Text color="#9ad5ff">{activeModel}</Text>
|
|
1675
|
-
</Box>
|
|
1676
|
-
<Box flexDirection="row">
|
|
1677
|
-
<Text color="#4a6070">voice </Text>
|
|
1678
|
-
<Text color={voiceEnabled ? "#9ad5ff" : "#5a7a9a"}>
|
|
1679
|
-
{voiceEnabled ? (voiceListening ? (voiceWaitingForSilence ? "on/waiting" : "on/listening") : "on") : "off"}
|
|
1680
|
-
</Text>
|
|
1681
|
-
</Box>
|
|
1682
|
-
</Box>
|
|
1683
|
-
<Text color="#2a3a50">{DIVIDER}</Text>
|
|
1684
|
-
<Text color="#3a5068">/help /new /history /voice /voice on|off|status /settings /logout /exit</Text>
|
|
1685
|
-
<Text color="#2a3a50">{DIVIDER}</Text>
|
|
1866
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1867
|
+
<Text color="#7a9bba">
|
|
1868
|
+
{`mode ${runtimeMode} · provider ${getProviderForModel(activeModel)} · model ${activeModel} · credits ${
|
|
1869
|
+
creditsRemaining ?? "--"
|
|
1870
|
+
}${lastCreditCost !== null ? ` (-${lastCreditCost})` : ""}`}
|
|
1871
|
+
</Text>
|
|
1872
|
+
<Text color="#6c88a8">
|
|
1873
|
+
{`scope ${
|
|
1874
|
+
workspaceRoot.length > HEADER_PATH_MAX ? `…${workspaceRoot.slice(-(HEADER_PATH_MAX - 1))}` : workspaceRoot
|
|
1875
|
+
} · voice ${
|
|
1876
|
+
voiceEnabled
|
|
1877
|
+
? voicePreparing
|
|
1878
|
+
? "on/preparing"
|
|
1879
|
+
: voiceListening
|
|
1880
|
+
? voiceWaitingForSilence
|
|
1881
|
+
? "on/waiting"
|
|
1882
|
+
: "on/listening"
|
|
1883
|
+
: "on/standby"
|
|
1884
|
+
: "off"
|
|
1885
|
+
}`}
|
|
1886
|
+
</Text>
|
|
1887
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1888
|
+
<Text color="#3a5068">/help /new /history /voice /dictate on|off|status</Text>
|
|
1889
|
+
<Text color="#3a5068">/tools compact|expanded /settings /logout /exit</Text>
|
|
1890
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1686
1891
|
<Box flexDirection="column" marginTop={1}>
|
|
1687
1892
|
{messages.map((message, index) => {
|
|
1893
|
+
const prev = index > 0 ? messages[index - 1] : null;
|
|
1688
1894
|
const style = styleForKind(message.kind);
|
|
1689
1895
|
const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
|
|
1690
1896
|
const isSpacing = message.text === "" && message.kind === "system";
|
|
1897
|
+
const needsGroupGap =
|
|
1898
|
+
Boolean(prev) &&
|
|
1899
|
+
prev?.kind !== "system" &&
|
|
1900
|
+
message.kind !== "system" &&
|
|
1901
|
+
prev?.kind !== message.kind &&
|
|
1902
|
+
(message.kind === "assistant" || message.kind === "tool");
|
|
1691
1903
|
if (isSpacing) {
|
|
1692
1904
|
return <Box key={`${index}-spacer`} marginTop={0}><Text> </Text></Box>;
|
|
1693
1905
|
}
|
|
1694
1906
|
|
|
1695
1907
|
if (message.kind === "tool" && message.card && !isSuperAdmin) {
|
|
1696
1908
|
const card = message.card;
|
|
1909
|
+
const localityLabel = card.locality === "LOCAL" ? "local" : "cloud";
|
|
1697
1910
|
const icon =
|
|
1698
1911
|
card.kind === "error"
|
|
1699
1912
|
? "✕"
|
|
@@ -1723,38 +1936,50 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1723
1936
|
? "#9ad5ff"
|
|
1724
1937
|
: "#9bc5ff";
|
|
1725
1938
|
return (
|
|
1726
|
-
<
|
|
1727
|
-
<Text
|
|
1728
|
-
<Box flexDirection="
|
|
1729
|
-
<Text color={
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
{line}
|
|
1939
|
+
<React.Fragment key={`${index}-${message.kind}`}>
|
|
1940
|
+
{needsGroupGap ? <Text> </Text> : null}
|
|
1941
|
+
<Box flexDirection="row">
|
|
1942
|
+
<Text color={style.labelColor}>{paddedLabel}</Text>
|
|
1943
|
+
<Box flexDirection="column">
|
|
1944
|
+
<Text color={accent}>
|
|
1945
|
+
{icon} {card.summary}
|
|
1946
|
+
{card.count && card.count > 1 ? <Text color="#9cb8d8"> (x{card.count})</Text> : null}
|
|
1947
|
+
<Text color="#5a7a9a"> · {localityLabel}</Text>
|
|
1736
1948
|
</Text>
|
|
1737
|
-
|
|
1949
|
+
{toolFeedMode === "expanded" ? (
|
|
1950
|
+
<>
|
|
1951
|
+
{card.path ? <Text color="#6c88a8">path: {card.path}</Text> : null}
|
|
1952
|
+
{(card.snippetLines ?? []).slice(0, TOOL_SNIPPET_LINES).map((line, idx) => (
|
|
1953
|
+
<Text key={`${index}-snippet-${idx}`} color="#8ea1bd">
|
|
1954
|
+
{line}
|
|
1955
|
+
</Text>
|
|
1956
|
+
))}
|
|
1957
|
+
</>
|
|
1958
|
+
) : null}
|
|
1959
|
+
</Box>
|
|
1738
1960
|
</Box>
|
|
1739
|
-
</
|
|
1961
|
+
</React.Fragment>
|
|
1740
1962
|
);
|
|
1741
1963
|
}
|
|
1742
1964
|
|
|
1743
1965
|
return (
|
|
1744
|
-
<
|
|
1745
|
-
<Text
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
<Box flexDirection="column">
|
|
1750
|
-
{renderMarkdownContent(message.text, style.textColor, `assistant-${index}`)}
|
|
1751
|
-
</Box>
|
|
1752
|
-
) : (
|
|
1753
|
-
<Text color={style.textColor} bold={style.bold && message.kind === "error"}>
|
|
1754
|
-
{message.text}
|
|
1966
|
+
<React.Fragment key={`${index}-${message.kind}`}>
|
|
1967
|
+
{needsGroupGap ? <Text> </Text> : null}
|
|
1968
|
+
<Box flexDirection="row">
|
|
1969
|
+
<Text color={style.labelColor} bold={style.bold}>
|
|
1970
|
+
{paddedLabel}
|
|
1755
1971
|
</Text>
|
|
1756
|
-
|
|
1757
|
-
|
|
1972
|
+
{message.kind === "assistant" ? (
|
|
1973
|
+
<Box flexDirection="column">
|
|
1974
|
+
{renderMarkdownContent(message.text, style.textColor, `assistant-${index}`)}
|
|
1975
|
+
</Box>
|
|
1976
|
+
) : (
|
|
1977
|
+
<Text color={style.textColor} bold={style.bold && message.kind === "error"}>
|
|
1978
|
+
{message.text}
|
|
1979
|
+
</Text>
|
|
1980
|
+
)}
|
|
1981
|
+
</Box>
|
|
1982
|
+
</React.Fragment>
|
|
1758
1983
|
);
|
|
1759
1984
|
})}
|
|
1760
1985
|
{streamingText ? (
|
|
@@ -1764,7 +1989,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1764
1989
|
</Box>
|
|
1765
1990
|
) : null}
|
|
1766
1991
|
</Box>
|
|
1767
|
-
<Text color="#2a3a50">{
|
|
1992
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1768
1993
|
{thinking ? (
|
|
1769
1994
|
<Box flexDirection="row" marginTop={1}>
|
|
1770
1995
|
<Text color="#7aa2ff">{"◆ astra".padEnd(LABEL_WIDTH, " ")}</Text>
|
|
@@ -1777,12 +2002,14 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1777
2002
|
{voiceEnabled && !thinking ? (
|
|
1778
2003
|
<Box flexDirection="row" marginTop={1}>
|
|
1779
2004
|
<Text color="#9ad5ff">{"🎤 voice".padEnd(LABEL_WIDTH, " ")}</Text>
|
|
1780
|
-
{
|
|
1781
|
-
<Text color="#
|
|
2005
|
+
{voicePreparing ? (
|
|
2006
|
+
<Text color="#f4d58a">🟡 preparing microphone...</Text>
|
|
2007
|
+
) : voiceListening && !voiceWaitingForSilence ? (
|
|
2008
|
+
<Text color="#9de3b4">🟢 listening now - speak clearly</Text>
|
|
1782
2009
|
) : voiceWaitingForSilence ? (
|
|
1783
|
-
<Text color="#b7c4d8"
|
|
2010
|
+
<Text color="#b7c4d8">⏳ speech detected - waiting for silence to send</Text>
|
|
1784
2011
|
) : (
|
|
1785
|
-
<Text color="#6f8199"
|
|
2012
|
+
<Text color="#6f8199">⚪ voice armed - preparing next listen window</Text>
|
|
1786
2013
|
)}
|
|
1787
2014
|
</Box>
|
|
1788
2015
|
) : null}
|