@astra-code/astra-ai 0.1.0 → 0.1.1
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/.env.example +3 -1
- package/README.md +1 -1
- package/dist/app/App.js +303 -50
- package/dist/app/App.js.map +1 -1
- package/dist/lib/backendClient.js +39 -2
- package/dist/lib/backendClient.js.map +1 -1
- package/dist/lib/voice.js +38 -3
- package/dist/lib/voice.js.map +1 -1
- package/package.json +1 -1
- package/src/app/App.tsx +409 -53
- package/src/lib/backendClient.ts +41 -2
- package/src/lib/voice.ts +40 -3
- package/src/types/events.ts +11 -2
package/src/app/App.tsx
CHANGED
|
@@ -17,17 +17,27 @@ import {
|
|
|
17
17
|
import {runTerminalCommand} from "../lib/terminalBridge.js";
|
|
18
18
|
import {isWorkspaceTrusted, trustWorkspace} from "../lib/trustStore.js";
|
|
19
19
|
import {scanWorkspace} from "../lib/workspaceScanner.js";
|
|
20
|
-
import {
|
|
20
|
+
import {startLiveTranscription, transcribeOnce, type LiveTranscriptionController} from "../lib/voice.js";
|
|
21
21
|
import type {AgentEvent, AuthSession, ChatMessage} from "../types/events.js";
|
|
22
22
|
import type {WorkspaceFile} from "../lib/workspaceScanner.js";
|
|
23
23
|
|
|
24
24
|
type UiMessage = {
|
|
25
25
|
kind: "system" | "user" | "assistant" | "tool" | "error";
|
|
26
26
|
text: string;
|
|
27
|
+
card?: ToolCard;
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
type HistoryMode = "picker" | "sessions";
|
|
30
31
|
|
|
32
|
+
type ToolCard = {
|
|
33
|
+
kind: "start" | "success" | "error" | "fileCreate" | "fileEdit" | "fileDelete" | "terminal" | "preview";
|
|
34
|
+
toolName: string;
|
|
35
|
+
locality: "LOCAL" | "REMOTE";
|
|
36
|
+
summary: string;
|
|
37
|
+
path?: string;
|
|
38
|
+
snippetLines?: string[];
|
|
39
|
+
};
|
|
40
|
+
|
|
31
41
|
// const ASTRA_ASCII = `
|
|
32
42
|
// █████╗ ███████╗████████╗██████╗ █████╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
33
43
|
// ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
|
@@ -63,10 +73,22 @@ const centerLine = (text: string, width = WELCOME_WIDTH): string => {
|
|
|
63
73
|
const FOUNDER_WELCOME = centerLine("Welcome to Astra from Astra CEO & Founder, Sean Donovan");
|
|
64
74
|
|
|
65
75
|
const HISTORY_SETTINGS_URL = "https://astra-web-builder.vercel.app/settings";
|
|
76
|
+
const VOICE_SILENCE_MS = Number(process.env.ASTRA_VOICE_SILENCE_MS ?? "3000");
|
|
77
|
+
const TOOL_SNIPPET_LINES = 6;
|
|
78
|
+
const NOISY_EVENT_TYPES = new Set([
|
|
79
|
+
"timing",
|
|
80
|
+
"session_info",
|
|
81
|
+
"axon_status",
|
|
82
|
+
"orchestration_plan",
|
|
83
|
+
"task_plan",
|
|
84
|
+
"task_update",
|
|
85
|
+
"preset_status"
|
|
86
|
+
]);
|
|
66
87
|
|
|
67
88
|
const eventToToolLine = (event: AgentEvent): string | null => {
|
|
68
89
|
if (event.type === "tool_start") {
|
|
69
|
-
const
|
|
90
|
+
const tool = event.tool as {name?: string} | undefined;
|
|
91
|
+
const name = tool?.name ?? "tool";
|
|
70
92
|
return `↳ ${name} executing...`;
|
|
71
93
|
}
|
|
72
94
|
if (event.type === "tool_result") {
|
|
@@ -77,6 +99,7 @@ const eventToToolLine = (event: AgentEvent): string | null => {
|
|
|
77
99
|
const output = String(
|
|
78
100
|
(payload.output as string | undefined) ??
|
|
79
101
|
(payload.content as string | undefined) ??
|
|
102
|
+
(payload.summary as string | undefined) ??
|
|
80
103
|
event.error ??
|
|
81
104
|
""
|
|
82
105
|
);
|
|
@@ -94,6 +117,28 @@ const eventToToolLine = (event: AgentEvent): string | null => {
|
|
|
94
117
|
return null;
|
|
95
118
|
};
|
|
96
119
|
|
|
120
|
+
const extractSnippetLines = (content: string, maxLines = TOOL_SNIPPET_LINES): string[] => {
|
|
121
|
+
if (!content) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const lines = content.split("\n").map((line) => line.trimEnd());
|
|
125
|
+
if (lines.length <= maxLines) {
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
return lines.slice(0, maxLines);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const extractDiffSnippet = (diff: unknown, maxLines = TOOL_SNIPPET_LINES): string[] => {
|
|
132
|
+
if (!Array.isArray(diff)) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const interesting = diff
|
|
136
|
+
.map((line) => String(line))
|
|
137
|
+
.filter((line) => line.startsWith("+") || line.startsWith("-"))
|
|
138
|
+
.map((line) => line.slice(0, 220));
|
|
139
|
+
return interesting.slice(0, maxLines);
|
|
140
|
+
};
|
|
141
|
+
|
|
97
142
|
const looksLikeLocalFilesystemClaim = (text: string): boolean => {
|
|
98
143
|
const lower = text.toLowerCase();
|
|
99
144
|
const changeWord =
|
|
@@ -518,6 +563,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
518
563
|
const [streamingText, setStreamingText] = useState("");
|
|
519
564
|
const [voiceEnabled, setVoiceEnabled] = useState(false);
|
|
520
565
|
const [voiceListening, setVoiceListening] = useState(false);
|
|
566
|
+
const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
|
|
567
|
+
const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState<string | null>(null);
|
|
521
568
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
522
569
|
const [historyMode, setHistoryMode] = useState<HistoryMode>("picker");
|
|
523
570
|
const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
|
|
@@ -526,11 +573,18 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
526
573
|
const [historyRows, setHistoryRows] = useState<SessionSummary[]>([]);
|
|
527
574
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
528
575
|
const liveVoiceRef = useRef<LiveTranscriptionController | null>(null);
|
|
576
|
+
const voiceSilenceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
577
|
+
const fileEditBuffersRef = useRef<Map<string, {path: string; chunks: string[]; toolName?: string}>>(new Map());
|
|
578
|
+
const isSuperAdmin = user?.role === "super_admin";
|
|
529
579
|
|
|
530
580
|
const pushMessage = useCallback((kind: UiMessage["kind"], text: string) => {
|
|
531
581
|
setMessages((prev) => [...prev, {kind, text}].slice(-300));
|
|
532
582
|
}, []);
|
|
533
583
|
|
|
584
|
+
const pushToolCard = useCallback((card: ToolCard) => {
|
|
585
|
+
setMessages((prev) => [...prev, {kind: "tool", text: card.summary, card} satisfies UiMessage].slice(-300));
|
|
586
|
+
}, []);
|
|
587
|
+
|
|
534
588
|
const filteredHistory = useMemo(() => {
|
|
535
589
|
const q = historyQuery.trim().toLowerCase();
|
|
536
590
|
if (!q) {
|
|
@@ -597,37 +651,70 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
597
651
|
}, [backend, pushMessage, user]);
|
|
598
652
|
|
|
599
653
|
const stopLiveVoice = useCallback(async (): Promise<void> => {
|
|
654
|
+
if (voiceSilenceTimerRef.current) {
|
|
655
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
656
|
+
voiceSilenceTimerRef.current = null;
|
|
657
|
+
}
|
|
600
658
|
const controller = liveVoiceRef.current;
|
|
601
659
|
if (!controller) {
|
|
660
|
+
setVoiceWaitingForSilence(false);
|
|
602
661
|
return;
|
|
603
662
|
}
|
|
604
663
|
liveVoiceRef.current = null;
|
|
605
664
|
await controller.stop();
|
|
606
665
|
setVoiceListening(false);
|
|
666
|
+
setVoiceWaitingForSilence(false);
|
|
607
667
|
}, []);
|
|
608
668
|
|
|
609
|
-
const startLiveVoice = useCallback((): void => {
|
|
669
|
+
const startLiveVoice = useCallback((announce = true): void => {
|
|
610
670
|
if (liveVoiceRef.current) {
|
|
611
671
|
return;
|
|
612
672
|
}
|
|
613
673
|
setVoiceEnabled(true);
|
|
614
674
|
setVoiceListening(true);
|
|
615
|
-
|
|
675
|
+
setVoiceWaitingForSilence(false);
|
|
676
|
+
if (announce) {
|
|
677
|
+
pushMessage("system", "Voice input started. Speak now…");
|
|
678
|
+
}
|
|
616
679
|
liveVoiceRef.current = startLiveTranscription({
|
|
617
680
|
onPartial: (text) => {
|
|
618
681
|
setPrompt(text);
|
|
682
|
+
if (voiceSilenceTimerRef.current) {
|
|
683
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
684
|
+
}
|
|
685
|
+
const candidate = text.trim();
|
|
686
|
+
if (!candidate) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
setVoiceWaitingForSilence(true);
|
|
690
|
+
voiceSilenceTimerRef.current = setTimeout(() => {
|
|
691
|
+
setVoiceQueuedPrompt(candidate);
|
|
692
|
+
void stopLiveVoice();
|
|
693
|
+
}, VOICE_SILENCE_MS);
|
|
619
694
|
},
|
|
620
695
|
onFinal: (text) => {
|
|
696
|
+
if (voiceSilenceTimerRef.current) {
|
|
697
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
698
|
+
voiceSilenceTimerRef.current = null;
|
|
699
|
+
}
|
|
621
700
|
setPrompt(text);
|
|
701
|
+
liveVoiceRef.current = null;
|
|
702
|
+
setVoiceListening(false);
|
|
703
|
+
setVoiceWaitingForSilence(false);
|
|
622
704
|
},
|
|
623
705
|
onError: (error) => {
|
|
706
|
+
setVoiceWaitingForSilence(false);
|
|
624
707
|
pushMessage("error", `Voice transcription error: ${error.message}`);
|
|
625
708
|
}
|
|
626
709
|
});
|
|
627
|
-
}, [pushMessage]);
|
|
710
|
+
}, [pushMessage, stopLiveVoice]);
|
|
628
711
|
|
|
629
712
|
useEffect(() => {
|
|
630
713
|
return () => {
|
|
714
|
+
if (voiceSilenceTimerRef.current) {
|
|
715
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
716
|
+
voiceSilenceTimerRef.current = null;
|
|
717
|
+
}
|
|
631
718
|
const controller = liveVoiceRef.current;
|
|
632
719
|
if (controller) {
|
|
633
720
|
void controller.stop();
|
|
@@ -635,6 +722,13 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
635
722
|
};
|
|
636
723
|
}, []);
|
|
637
724
|
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
if (!voiceEnabled || !user || thinking || voiceListening || liveVoiceRef.current) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
startLiveVoice(false);
|
|
730
|
+
}, [startLiveVoice, thinking, user, voiceEnabled, voiceListening]);
|
|
731
|
+
|
|
638
732
|
useEffect(() => {
|
|
639
733
|
if (!trustedWorkspace) {
|
|
640
734
|
return;
|
|
@@ -652,6 +746,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
652
746
|
|
|
653
747
|
const cached = loadSession();
|
|
654
748
|
if (!cached) {
|
|
749
|
+
backend.setAuthSession(null);
|
|
655
750
|
if (!cancelled) {
|
|
656
751
|
setBooting(false);
|
|
657
752
|
}
|
|
@@ -661,8 +756,12 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
661
756
|
const valid = await backend.validateSession(cached);
|
|
662
757
|
if (!valid) {
|
|
663
758
|
clearSession();
|
|
759
|
+
backend.setAuthSession(null);
|
|
664
760
|
} else if (!cancelled) {
|
|
665
|
-
|
|
761
|
+
const hydrated = await backend.getUserProfile(cached).catch(() => cached);
|
|
762
|
+
saveSession(hydrated);
|
|
763
|
+
backend.setAuthSession(hydrated);
|
|
764
|
+
setUser(hydrated);
|
|
666
765
|
setMessages([
|
|
667
766
|
{kind: "system", text: "Welcome back. Type a message or /help."},
|
|
668
767
|
{kind: "system", text: ""}
|
|
@@ -796,7 +895,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
796
895
|
try {
|
|
797
896
|
await backend.deleteSession(selected.id);
|
|
798
897
|
setHistoryRows((prev) => prev.filter((row) => row.id !== selected.id));
|
|
799
|
-
pushMessage("
|
|
898
|
+
pushMessage("system", `Archived session: ${selected.title}`);
|
|
800
899
|
} catch (error) {
|
|
801
900
|
pushMessage("error", `Failed to delete session: ${error instanceof Error ? error.message : String(error)}`);
|
|
802
901
|
}
|
|
@@ -833,8 +932,10 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
833
932
|
}
|
|
834
933
|
|
|
835
934
|
const authSession = data as AuthSession;
|
|
836
|
-
|
|
837
|
-
|
|
935
|
+
const hydrated = await backend.getUserProfile(authSession).catch(() => authSession);
|
|
936
|
+
saveSession(hydrated);
|
|
937
|
+
backend.setAuthSession(hydrated);
|
|
938
|
+
setUser(hydrated);
|
|
838
939
|
setEmail("");
|
|
839
940
|
setPassword("");
|
|
840
941
|
setMessages([
|
|
@@ -850,8 +951,91 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
850
951
|
|
|
851
952
|
const handleEvent = useCallback(
|
|
852
953
|
async (event: AgentEvent, activeSessionId: string): Promise<string | null> => {
|
|
954
|
+
if (event.type === "file_edit_start") {
|
|
955
|
+
const data = (event.data ?? {}) as Record<string, unknown>;
|
|
956
|
+
const toolId =
|
|
957
|
+
typeof event.tool_id === "string"
|
|
958
|
+
? event.tool_id
|
|
959
|
+
: typeof data.tool_id === "string"
|
|
960
|
+
? data.tool_id
|
|
961
|
+
: "";
|
|
962
|
+
const toolName =
|
|
963
|
+
typeof event.tool_name === "string"
|
|
964
|
+
? event.tool_name
|
|
965
|
+
: typeof data.tool_name === "string"
|
|
966
|
+
? data.tool_name
|
|
967
|
+
: undefined;
|
|
968
|
+
if (toolId) {
|
|
969
|
+
const current = fileEditBuffersRef.current.get(toolId) ?? {path: "(unknown)", chunks: []};
|
|
970
|
+
fileEditBuffersRef.current.set(toolId, {...current, toolName});
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (event.type === "file_edit_path") {
|
|
976
|
+
const data = (event.data ?? {}) as Record<string, unknown>;
|
|
977
|
+
const toolId =
|
|
978
|
+
typeof event.tool_id === "string"
|
|
979
|
+
? event.tool_id
|
|
980
|
+
: typeof data.tool_id === "string"
|
|
981
|
+
? data.tool_id
|
|
982
|
+
: "";
|
|
983
|
+
const path =
|
|
984
|
+
typeof event.path === "string"
|
|
985
|
+
? event.path
|
|
986
|
+
: typeof data.path === "string"
|
|
987
|
+
? data.path
|
|
988
|
+
: "(unknown)";
|
|
989
|
+
const toolName =
|
|
990
|
+
typeof event.tool_name === "string"
|
|
991
|
+
? event.tool_name
|
|
992
|
+
: typeof data.tool_name === "string"
|
|
993
|
+
? data.tool_name
|
|
994
|
+
: undefined;
|
|
995
|
+
if (toolId) {
|
|
996
|
+
const current = fileEditBuffersRef.current.get(toolId);
|
|
997
|
+
fileEditBuffersRef.current.set(toolId, {path, chunks: [], toolName: toolName ?? current?.toolName});
|
|
998
|
+
}
|
|
999
|
+
if (!isSuperAdmin) {
|
|
1000
|
+
const resolvedToolName = toolName ?? fileEditBuffersRef.current.get(toolId)?.toolName;
|
|
1001
|
+
const usingBulkWriter = resolvedToolName === "bulk_file_writer";
|
|
1002
|
+
pushToolCard({
|
|
1003
|
+
kind: "fileEdit",
|
|
1004
|
+
toolName: resolvedToolName ?? "edit_file",
|
|
1005
|
+
locality: "LOCAL",
|
|
1006
|
+
summary: usingBulkWriter ? `Writing file ${path}` : `Editing ${path}`,
|
|
1007
|
+
path
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (event.type === "file_edit_delta") {
|
|
1014
|
+
const data = (event.data ?? {}) as Record<string, unknown>;
|
|
1015
|
+
const toolId =
|
|
1016
|
+
typeof event.tool_id === "string"
|
|
1017
|
+
? event.tool_id
|
|
1018
|
+
: typeof data.tool_id === "string"
|
|
1019
|
+
? data.tool_id
|
|
1020
|
+
: "";
|
|
1021
|
+
const chunk = typeof event.content === "string" ? event.content : "";
|
|
1022
|
+
if (toolId && chunk) {
|
|
1023
|
+
const current = fileEditBuffersRef.current.get(toolId) ?? {path: "(unknown)", chunks: [], toolName: undefined};
|
|
1024
|
+
current.chunks.push(chunk);
|
|
1025
|
+
fileEditBuffersRef.current.set(toolId, current);
|
|
1026
|
+
}
|
|
1027
|
+
if (isSuperAdmin) {
|
|
1028
|
+
const raw = JSON.stringify(event);
|
|
1029
|
+
pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
|
|
1030
|
+
}
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
853
1034
|
const assistantPiece = extractAssistantText(event);
|
|
854
|
-
if (
|
|
1035
|
+
if (
|
|
1036
|
+
assistantPiece &&
|
|
1037
|
+
!["tool_result", "error", "credits_update", "credits_exhausted", "file_edit_delta", "file_edit_path"].includes(event.type)
|
|
1038
|
+
) {
|
|
855
1039
|
return assistantPiece;
|
|
856
1040
|
}
|
|
857
1041
|
|
|
@@ -872,22 +1056,45 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
872
1056
|
});
|
|
873
1057
|
}
|
|
874
1058
|
|
|
875
|
-
|
|
1059
|
+
if (isSuperAdmin) {
|
|
1060
|
+
pushMessage("tool", `[LOCAL] ▸ ran: ${command.slice(0, 60)}...`);
|
|
1061
|
+
} else {
|
|
1062
|
+
pushToolCard({
|
|
1063
|
+
kind: "terminal",
|
|
1064
|
+
toolName: "run_in_terminal",
|
|
1065
|
+
locality: "LOCAL",
|
|
1066
|
+
summary: `Running command: ${command.slice(0, 80)}`
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
876
1069
|
return null;
|
|
877
1070
|
}
|
|
878
1071
|
|
|
879
1072
|
// Apply file operations to the local workspace immediately.
|
|
880
1073
|
if (event.type === "tool_result" && event.success) {
|
|
881
1074
|
const d = (event.data ?? {}) as Record<string, unknown>;
|
|
882
|
-
const resultType =
|
|
1075
|
+
const resultType = event.result_type;
|
|
1076
|
+
const toolResultId = typeof event.tool_call_id === "string" ? event.tool_call_id : "";
|
|
883
1077
|
const relPath = typeof d.path === "string" ? d.path : null;
|
|
884
1078
|
const lang = typeof d.language === "string" ? d.language : "plaintext";
|
|
1079
|
+
const locality: "LOCAL" | "REMOTE" = d.local === true ? "LOCAL" : "REMOTE";
|
|
885
1080
|
|
|
886
1081
|
if (relPath) {
|
|
887
1082
|
if (resultType === "file_create") {
|
|
888
1083
|
const content = typeof d.content === "string" ? d.content : "";
|
|
889
1084
|
writeLocalFile(relPath, content, lang);
|
|
890
|
-
|
|
1085
|
+
const snippetLines = extractSnippetLines(content);
|
|
1086
|
+
if (isSuperAdmin) {
|
|
1087
|
+
pushMessage("tool", `[LOCAL] ✓ wrote ${relPath}`);
|
|
1088
|
+
} else {
|
|
1089
|
+
pushToolCard({
|
|
1090
|
+
kind: "fileCreate",
|
|
1091
|
+
toolName: "create_file",
|
|
1092
|
+
locality,
|
|
1093
|
+
summary: `Created ${relPath}`,
|
|
1094
|
+
path: relPath,
|
|
1095
|
+
snippetLines
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
891
1098
|
} else if (resultType === "file_edit") {
|
|
892
1099
|
const content =
|
|
893
1100
|
typeof d.full_new_content === "string"
|
|
@@ -897,13 +1104,61 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
897
1104
|
: null;
|
|
898
1105
|
if (content !== null) {
|
|
899
1106
|
writeLocalFile(relPath, content, lang);
|
|
900
|
-
|
|
1107
|
+
const snippetLines = extractDiffSnippet(d.diff) ?? extractSnippetLines(content);
|
|
1108
|
+
if (isSuperAdmin) {
|
|
1109
|
+
pushMessage("tool", `[LOCAL] ✓ edited ${relPath}`);
|
|
1110
|
+
} else {
|
|
1111
|
+
const toolId = toolResultId;
|
|
1112
|
+
const streamed = toolId ? fileEditBuffersRef.current.get(toolId) : undefined;
|
|
1113
|
+
const streamedSnippet = streamed ? extractSnippetLines(streamed.chunks.join("")) : [];
|
|
1114
|
+
pushToolCard({
|
|
1115
|
+
kind: "fileEdit",
|
|
1116
|
+
toolName: "edit_file",
|
|
1117
|
+
locality,
|
|
1118
|
+
summary: `Updated ${relPath}`,
|
|
1119
|
+
path: relPath,
|
|
1120
|
+
snippetLines: streamedSnippet.length > 0 ? streamedSnippet : snippetLines
|
|
1121
|
+
});
|
|
1122
|
+
if (toolId) {
|
|
1123
|
+
fileEditBuffersRef.current.delete(toolId);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
901
1126
|
}
|
|
902
1127
|
} else if (resultType === "file_delete") {
|
|
903
1128
|
deleteLocalFile(relPath);
|
|
904
|
-
|
|
1129
|
+
if (isSuperAdmin) {
|
|
1130
|
+
pushMessage("tool", `[LOCAL] ✓ deleted ${relPath}`);
|
|
1131
|
+
} else {
|
|
1132
|
+
pushToolCard({
|
|
1133
|
+
kind: "fileDelete",
|
|
1134
|
+
toolName: "delete_file",
|
|
1135
|
+
locality,
|
|
1136
|
+
summary: `Deleted ${relPath}`,
|
|
1137
|
+
path: relPath
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
905
1140
|
}
|
|
906
1141
|
}
|
|
1142
|
+
|
|
1143
|
+
if (!isSuperAdmin && event.tool_name === "start_preview") {
|
|
1144
|
+
const message =
|
|
1145
|
+
typeof d.message === "string"
|
|
1146
|
+
? d.message
|
|
1147
|
+
: typeof d.preview_url === "string"
|
|
1148
|
+
? `Preview: ${d.preview_url}`
|
|
1149
|
+
: "Local preview started.";
|
|
1150
|
+
pushToolCard({
|
|
1151
|
+
kind: "preview",
|
|
1152
|
+
toolName: "start_preview",
|
|
1153
|
+
locality,
|
|
1154
|
+
summary: message
|
|
1155
|
+
});
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (toolResultId) {
|
|
1160
|
+
fileEditBuffersRef.current.delete(toolResultId);
|
|
1161
|
+
}
|
|
907
1162
|
}
|
|
908
1163
|
|
|
909
1164
|
if (event.type === "credits_update") {
|
|
@@ -922,20 +1177,49 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
922
1177
|
setCreditsRemaining(0);
|
|
923
1178
|
}
|
|
924
1179
|
|
|
1180
|
+
if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (!isSuperAdmin && event.type === "tool_start") {
|
|
1185
|
+
const tool = event.tool as {name?: string} | undefined;
|
|
1186
|
+
const name = tool?.name ?? "tool";
|
|
1187
|
+
pushToolCard({
|
|
1188
|
+
kind: "start",
|
|
1189
|
+
toolName: name,
|
|
1190
|
+
locality: "REMOTE",
|
|
1191
|
+
summary: `${name} is running...`
|
|
1192
|
+
});
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
925
1196
|
const toolLine = eventToToolLine(event);
|
|
926
1197
|
if (toolLine) {
|
|
927
1198
|
if (event.type === "error" || event.type === "credits_exhausted") {
|
|
928
1199
|
pushMessage("error", toolLine);
|
|
929
1200
|
} else {
|
|
930
|
-
|
|
1201
|
+
if (isSuperAdmin) {
|
|
1202
|
+
pushMessage("tool", toolLine);
|
|
1203
|
+
} else if (event.type === "tool_result") {
|
|
1204
|
+
const mark = event.success ? "completed" : "failed";
|
|
1205
|
+
const toolName = typeof event.tool_name === "string" ? event.tool_name : "tool";
|
|
1206
|
+
pushToolCard({
|
|
1207
|
+
kind: event.success ? "success" : "error",
|
|
1208
|
+
toolName,
|
|
1209
|
+
locality: ((event.data ?? {}) as Record<string, unknown>).local === true ? "LOCAL" : "REMOTE",
|
|
1210
|
+
summary: `${toolName} ${mark}`
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
931
1213
|
}
|
|
932
1214
|
} else if (event.type !== "thinking") {
|
|
933
|
-
|
|
934
|
-
|
|
1215
|
+
if (isSuperAdmin) {
|
|
1216
|
+
const raw = JSON.stringify(event);
|
|
1217
|
+
pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
|
|
1218
|
+
}
|
|
935
1219
|
}
|
|
936
1220
|
return null;
|
|
937
1221
|
},
|
|
938
|
-
[backend, deleteLocalFile, pushMessage, writeLocalFile]
|
|
1222
|
+
[backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]
|
|
939
1223
|
);
|
|
940
1224
|
|
|
941
1225
|
const sendPrompt = useCallback(
|
|
@@ -948,7 +1232,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
948
1232
|
if (text === "/help") {
|
|
949
1233
|
pushMessage(
|
|
950
1234
|
"system",
|
|
951
|
-
"/new /history /voice on|off|status
|
|
1235
|
+
"/new /history /voice /voice on|off|status /settings /settings model <id> /logout /exit"
|
|
952
1236
|
);
|
|
953
1237
|
pushMessage("system", "");
|
|
954
1238
|
return;
|
|
@@ -956,7 +1240,20 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
956
1240
|
if (text === "/settings") {
|
|
957
1241
|
pushMessage(
|
|
958
1242
|
"system",
|
|
959
|
-
`Settings: mode=${runtimeMode} scope=${workspaceRoot} model=${activeModel} provider=${getProviderForModel(activeModel)} voice=${voiceEnabled ? "on" : "off"} listening=${voiceListening ? "yes" : "no"} client_id=${getDefaultClientId()} backend=${getBackendUrl()}`
|
|
1243
|
+
`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()}`
|
|
1244
|
+
);
|
|
1245
|
+
pushMessage("system", "");
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (text === "/voice") {
|
|
1249
|
+
if (!voiceEnabled) {
|
|
1250
|
+
setVoiceEnabled(true);
|
|
1251
|
+
startLiveVoice(true);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
pushMessage(
|
|
1255
|
+
"system",
|
|
1256
|
+
`Voice input is on${voiceListening ? " (currently listening)" : ""}. Use /voice off to disable.`
|
|
960
1257
|
);
|
|
961
1258
|
pushMessage("system", "");
|
|
962
1259
|
return;
|
|
@@ -984,39 +1281,22 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
984
1281
|
if (text === "/voice status") {
|
|
985
1282
|
pushMessage(
|
|
986
1283
|
"system",
|
|
987
|
-
`Voice
|
|
1284
|
+
`Voice input is ${voiceEnabled ? "on" : "off"}${voiceListening ? " (live transcription active)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`
|
|
988
1285
|
);
|
|
989
1286
|
pushMessage("system", "");
|
|
990
1287
|
return;
|
|
991
1288
|
}
|
|
992
1289
|
if (text === "/voice on") {
|
|
993
1290
|
setVoiceEnabled(true);
|
|
994
|
-
|
|
1291
|
+
startLiveVoice(true);
|
|
1292
|
+
pushMessage("system", `Voice input enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
|
|
995
1293
|
pushMessage("system", "");
|
|
996
1294
|
return;
|
|
997
1295
|
}
|
|
998
1296
|
if (text === "/voice off") {
|
|
999
1297
|
await stopLiveVoice();
|
|
1000
1298
|
setVoiceEnabled(false);
|
|
1001
|
-
pushMessage("system", "Voice
|
|
1002
|
-
pushMessage("system", "");
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
if (text === "/voice start") {
|
|
1006
|
-
if (voiceListening) {
|
|
1007
|
-
pushMessage("system", "Live transcription is already running.");
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
startLiveVoice();
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
if (text === "/voice stop") {
|
|
1014
|
-
if (!voiceListening) {
|
|
1015
|
-
pushMessage("system", "Live transcription is not running.");
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
await stopLiveVoice();
|
|
1019
|
-
pushMessage("system", "Live transcription stopped. Press Enter to send transcript.");
|
|
1299
|
+
pushMessage("system", "Voice input disabled.");
|
|
1020
1300
|
pushMessage("system", "");
|
|
1021
1301
|
return;
|
|
1022
1302
|
}
|
|
@@ -1025,13 +1305,11 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1025
1305
|
if (!transcribed) {
|
|
1026
1306
|
pushMessage(
|
|
1027
1307
|
"error",
|
|
1028
|
-
"No speech transcribed.
|
|
1308
|
+
"No speech transcribed. Check OPENAI_API_KEY and mic capture command (optional ASTRA_STT_CAPTURE_COMMAND)."
|
|
1029
1309
|
);
|
|
1030
1310
|
return;
|
|
1031
1311
|
}
|
|
1032
|
-
|
|
1033
|
-
setPrompt(transcribed);
|
|
1034
|
-
pushMessage("system", "Transcribed input ready. Press Enter to send.");
|
|
1312
|
+
setVoiceQueuedPrompt(transcribed.trim());
|
|
1035
1313
|
return;
|
|
1036
1314
|
}
|
|
1037
1315
|
if (text === "/new") {
|
|
@@ -1045,6 +1323,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1045
1323
|
if (text === "/logout") {
|
|
1046
1324
|
await stopLiveVoice();
|
|
1047
1325
|
clearSession();
|
|
1326
|
+
backend.setAuthSession(null);
|
|
1048
1327
|
setUser(null);
|
|
1049
1328
|
setMessages([]);
|
|
1050
1329
|
setChatMessages([]);
|
|
@@ -1113,9 +1392,6 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1113
1392
|
? `Remote result (not yet confirmed as local filesystem change): ${cleanedAssistant}`
|
|
1114
1393
|
: cleanedAssistant;
|
|
1115
1394
|
pushMessage("assistant", guardedAssistant);
|
|
1116
|
-
if (voiceEnabled) {
|
|
1117
|
-
speakText(guardedAssistant);
|
|
1118
|
-
}
|
|
1119
1395
|
setChatMessages((prev) => [...prev, {role: "assistant", content: cleanedAssistant}]);
|
|
1120
1396
|
} else {
|
|
1121
1397
|
setChatMessages((prev) => [...prev, {role: "assistant", content: assistant}]);
|
|
@@ -1143,10 +1419,25 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1143
1419
|
user,
|
|
1144
1420
|
voiceEnabled,
|
|
1145
1421
|
voiceListening,
|
|
1422
|
+
voiceWaitingForSilence,
|
|
1146
1423
|
workspaceRoot
|
|
1147
1424
|
]
|
|
1148
1425
|
);
|
|
1149
1426
|
|
|
1427
|
+
useEffect(() => {
|
|
1428
|
+
if (!voiceQueuedPrompt || !user || thinking) {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const queued = voiceQueuedPrompt.trim();
|
|
1432
|
+
setVoiceQueuedPrompt(null);
|
|
1433
|
+
if (!queued) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
pushMessage("system", `Voice input: ${queued}`);
|
|
1437
|
+
setPrompt("");
|
|
1438
|
+
void sendPrompt(queued);
|
|
1439
|
+
}, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
|
|
1440
|
+
|
|
1150
1441
|
if (!trustedWorkspace) {
|
|
1151
1442
|
return (
|
|
1152
1443
|
<Box flexDirection="column">
|
|
@@ -1363,12 +1654,12 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1363
1654
|
<Box flexDirection="row">
|
|
1364
1655
|
<Text color="#4a6070">voice </Text>
|
|
1365
1656
|
<Text color={voiceEnabled ? "#9ad5ff" : "#5a7a9a"}>
|
|
1366
|
-
{voiceEnabled ? (voiceListening ? "on/listening" : "on") : "off"}
|
|
1657
|
+
{voiceEnabled ? (voiceListening ? (voiceWaitingForSilence ? "on/waiting" : "on/listening") : "on") : "off"}
|
|
1367
1658
|
</Text>
|
|
1368
1659
|
</Box>
|
|
1369
1660
|
</Box>
|
|
1370
1661
|
<Text color="#2a3a50">{DIVIDER}</Text>
|
|
1371
|
-
<Text color="#3a5068">/help /new /history /voice on|off|status
|
|
1662
|
+
<Text color="#3a5068">/help /new /history /voice /voice on|off|status /settings /logout /exit</Text>
|
|
1372
1663
|
<Text color="#2a3a50">{DIVIDER}</Text>
|
|
1373
1664
|
<Box flexDirection="column" marginTop={1}>
|
|
1374
1665
|
{messages.map((message, index) => {
|
|
@@ -1378,6 +1669,55 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1378
1669
|
if (isSpacing) {
|
|
1379
1670
|
return <Box key={`${index}-spacer`} marginTop={0}><Text> </Text></Box>;
|
|
1380
1671
|
}
|
|
1672
|
+
|
|
1673
|
+
if (message.kind === "tool" && message.card && !isSuperAdmin) {
|
|
1674
|
+
const card = message.card;
|
|
1675
|
+
const icon =
|
|
1676
|
+
card.kind === "error"
|
|
1677
|
+
? "✕"
|
|
1678
|
+
: card.kind === "fileDelete"
|
|
1679
|
+
? "🗑"
|
|
1680
|
+
: card.kind === "fileCreate"
|
|
1681
|
+
? "+"
|
|
1682
|
+
: card.kind === "fileEdit"
|
|
1683
|
+
? "✎"
|
|
1684
|
+
: card.kind === "preview"
|
|
1685
|
+
? "◉"
|
|
1686
|
+
: card.kind === "terminal"
|
|
1687
|
+
? "⌘"
|
|
1688
|
+
: card.kind === "start"
|
|
1689
|
+
? "…"
|
|
1690
|
+
: "✓";
|
|
1691
|
+
const accent =
|
|
1692
|
+
card.kind === "error"
|
|
1693
|
+
? "#ff8d8d"
|
|
1694
|
+
: card.kind === "fileDelete"
|
|
1695
|
+
? "#8a96a6"
|
|
1696
|
+
: card.kind === "fileEdit"
|
|
1697
|
+
? "#b7d4ff"
|
|
1698
|
+
: card.kind === "fileCreate"
|
|
1699
|
+
? "#a5e9c5"
|
|
1700
|
+
: card.kind === "preview"
|
|
1701
|
+
? "#9ad5ff"
|
|
1702
|
+
: "#9bc5ff";
|
|
1703
|
+
return (
|
|
1704
|
+
<Box key={`${index}-${message.kind}`} flexDirection="row">
|
|
1705
|
+
<Text color={style.labelColor}>{paddedLabel}</Text>
|
|
1706
|
+
<Box flexDirection="column">
|
|
1707
|
+
<Text color={accent}>
|
|
1708
|
+
{icon} {card.summary} <Text color="#5a7a9a">[{card.locality}]</Text>
|
|
1709
|
+
</Text>
|
|
1710
|
+
{card.path ? <Text color="#6c88a8">path: {card.path}</Text> : null}
|
|
1711
|
+
{(card.snippetLines ?? []).slice(0, TOOL_SNIPPET_LINES).map((line, idx) => (
|
|
1712
|
+
<Text key={`${index}-snippet-${idx}`} color="#8ea1bd">
|
|
1713
|
+
{line}
|
|
1714
|
+
</Text>
|
|
1715
|
+
))}
|
|
1716
|
+
</Box>
|
|
1717
|
+
</Box>
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1381
1721
|
return (
|
|
1382
1722
|
<Box key={`${index}-${message.kind}`} flexDirection="row">
|
|
1383
1723
|
<Text color={style.labelColor} bold={style.bold}>
|
|
@@ -1412,16 +1752,32 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1412
1752
|
</Text>
|
|
1413
1753
|
</Box>
|
|
1414
1754
|
) : null}
|
|
1755
|
+
{voiceEnabled && !thinking ? (
|
|
1756
|
+
<Box flexDirection="row" marginTop={1}>
|
|
1757
|
+
<Text color="#9ad5ff">{"🎤 voice".padEnd(LABEL_WIDTH, " ")}</Text>
|
|
1758
|
+
{voiceListening && !voiceWaitingForSilence ? (
|
|
1759
|
+
<Text color="#a6d9ff">🎙 listening... goblin ears activated 👂</Text>
|
|
1760
|
+
) : voiceWaitingForSilence ? (
|
|
1761
|
+
<Text color="#b7c4d8">⏸ waiting for silence... dramatic pause loading...</Text>
|
|
1762
|
+
) : (
|
|
1763
|
+
<Text color="#6f8199">voice armed... say something when ready</Text>
|
|
1764
|
+
)}
|
|
1765
|
+
</Box>
|
|
1766
|
+
) : null}
|
|
1415
1767
|
<Box marginTop={1} flexDirection="row">
|
|
1416
1768
|
<Text color="#7aa2ff">❯ </Text>
|
|
1417
1769
|
<TextInput
|
|
1418
1770
|
value={prompt}
|
|
1419
|
-
onChange={setPrompt}
|
|
1420
1771
|
onSubmit={(value) => {
|
|
1421
1772
|
setPrompt("");
|
|
1422
1773
|
void sendPrompt(value);
|
|
1423
1774
|
}}
|
|
1424
|
-
|
|
1775
|
+
onChange={(value) => {
|
|
1776
|
+
if (!voiceListening) {
|
|
1777
|
+
setPrompt(value);
|
|
1778
|
+
}
|
|
1779
|
+
}}
|
|
1780
|
+
placeholder={voiceEnabled ? "Ask Astra... (voice on: auto listen + send on silence)" : "Ask Astra..."}
|
|
1425
1781
|
/>
|
|
1426
1782
|
</Box>
|
|
1427
1783
|
</Box>
|