@astra-code/astra-ai 0.1.0 → 0.1.2
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 +6 -1
- package/README.md +13 -2
- package/dist/app/App.js +366 -51
- 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/config.js +10 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/voice.js +48 -11
- package/dist/lib/voice.js.map +1 -1
- package/package.json +1 -1
- package/src/app/App.tsx +429 -53
- package/src/lib/backendClient.ts +41 -2
- package/src/lib/config.ts +12 -1
- package/src/lib/voice.ts +51 -15
- 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") {
|
|
@@ -74,9 +96,12 @@ const eventToToolLine = (event: AgentEvent): string | null => {
|
|
|
74
96
|
const mark = success ? "✓" : "✗";
|
|
75
97
|
const toolName = event.tool_name ?? "tool";
|
|
76
98
|
const payload = (event.data ?? {}) as Record<string, unknown>;
|
|
99
|
+
const screenshotPath = typeof payload.path === "string" ? payload.path : "";
|
|
77
100
|
const output = String(
|
|
78
101
|
(payload.output as string | undefined) ??
|
|
79
102
|
(payload.content as string | undefined) ??
|
|
103
|
+
(payload.summary as string | undefined) ??
|
|
104
|
+
(toolName === "capture_screenshot" && screenshotPath ? `Saved screenshot: ${screenshotPath}` : undefined) ??
|
|
80
105
|
event.error ??
|
|
81
106
|
""
|
|
82
107
|
);
|
|
@@ -94,6 +119,28 @@ const eventToToolLine = (event: AgentEvent): string | null => {
|
|
|
94
119
|
return null;
|
|
95
120
|
};
|
|
96
121
|
|
|
122
|
+
const extractSnippetLines = (content: string, maxLines = TOOL_SNIPPET_LINES): string[] => {
|
|
123
|
+
if (!content) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const lines = content.split("\n").map((line) => line.trimEnd());
|
|
127
|
+
if (lines.length <= maxLines) {
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
return lines.slice(0, maxLines);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const extractDiffSnippet = (diff: unknown, maxLines = TOOL_SNIPPET_LINES): string[] => {
|
|
134
|
+
if (!Array.isArray(diff)) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const interesting = diff
|
|
138
|
+
.map((line) => String(line))
|
|
139
|
+
.filter((line) => line.startsWith("+") || line.startsWith("-"))
|
|
140
|
+
.map((line) => line.slice(0, 220));
|
|
141
|
+
return interesting.slice(0, maxLines);
|
|
142
|
+
};
|
|
143
|
+
|
|
97
144
|
const looksLikeLocalFilesystemClaim = (text: string): boolean => {
|
|
98
145
|
const lower = text.toLowerCase();
|
|
99
146
|
const changeWord =
|
|
@@ -518,6 +565,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
518
565
|
const [streamingText, setStreamingText] = useState("");
|
|
519
566
|
const [voiceEnabled, setVoiceEnabled] = useState(false);
|
|
520
567
|
const [voiceListening, setVoiceListening] = useState(false);
|
|
568
|
+
const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
|
|
569
|
+
const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState<string | null>(null);
|
|
521
570
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
522
571
|
const [historyMode, setHistoryMode] = useState<HistoryMode>("picker");
|
|
523
572
|
const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
|
|
@@ -526,11 +575,18 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
526
575
|
const [historyRows, setHistoryRows] = useState<SessionSummary[]>([]);
|
|
527
576
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
528
577
|
const liveVoiceRef = useRef<LiveTranscriptionController | null>(null);
|
|
578
|
+
const voiceSilenceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
579
|
+
const fileEditBuffersRef = useRef<Map<string, {path: string; chunks: string[]; toolName: string | undefined}>>(new Map());
|
|
580
|
+
const isSuperAdmin = user?.role === "super_admin";
|
|
529
581
|
|
|
530
582
|
const pushMessage = useCallback((kind: UiMessage["kind"], text: string) => {
|
|
531
583
|
setMessages((prev) => [...prev, {kind, text}].slice(-300));
|
|
532
584
|
}, []);
|
|
533
585
|
|
|
586
|
+
const pushToolCard = useCallback((card: ToolCard) => {
|
|
587
|
+
setMessages((prev) => [...prev, {kind: "tool", text: card.summary, card} satisfies UiMessage].slice(-300));
|
|
588
|
+
}, []);
|
|
589
|
+
|
|
534
590
|
const filteredHistory = useMemo(() => {
|
|
535
591
|
const q = historyQuery.trim().toLowerCase();
|
|
536
592
|
if (!q) {
|
|
@@ -597,37 +653,70 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
597
653
|
}, [backend, pushMessage, user]);
|
|
598
654
|
|
|
599
655
|
const stopLiveVoice = useCallback(async (): Promise<void> => {
|
|
656
|
+
if (voiceSilenceTimerRef.current) {
|
|
657
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
658
|
+
voiceSilenceTimerRef.current = null;
|
|
659
|
+
}
|
|
600
660
|
const controller = liveVoiceRef.current;
|
|
601
661
|
if (!controller) {
|
|
662
|
+
setVoiceWaitingForSilence(false);
|
|
602
663
|
return;
|
|
603
664
|
}
|
|
604
665
|
liveVoiceRef.current = null;
|
|
605
666
|
await controller.stop();
|
|
606
667
|
setVoiceListening(false);
|
|
668
|
+
setVoiceWaitingForSilence(false);
|
|
607
669
|
}, []);
|
|
608
670
|
|
|
609
|
-
const startLiveVoice = useCallback((): void => {
|
|
671
|
+
const startLiveVoice = useCallback((announce = true): void => {
|
|
610
672
|
if (liveVoiceRef.current) {
|
|
611
673
|
return;
|
|
612
674
|
}
|
|
613
675
|
setVoiceEnabled(true);
|
|
614
676
|
setVoiceListening(true);
|
|
615
|
-
|
|
677
|
+
setVoiceWaitingForSilence(false);
|
|
678
|
+
if (announce) {
|
|
679
|
+
pushMessage("system", "Voice input started. Speak now…");
|
|
680
|
+
}
|
|
616
681
|
liveVoiceRef.current = startLiveTranscription({
|
|
617
682
|
onPartial: (text) => {
|
|
618
683
|
setPrompt(text);
|
|
684
|
+
if (voiceSilenceTimerRef.current) {
|
|
685
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
686
|
+
}
|
|
687
|
+
const candidate = text.trim();
|
|
688
|
+
if (!candidate) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
setVoiceWaitingForSilence(true);
|
|
692
|
+
voiceSilenceTimerRef.current = setTimeout(() => {
|
|
693
|
+
setVoiceQueuedPrompt(candidate);
|
|
694
|
+
void stopLiveVoice();
|
|
695
|
+
}, VOICE_SILENCE_MS);
|
|
619
696
|
},
|
|
620
697
|
onFinal: (text) => {
|
|
698
|
+
if (voiceSilenceTimerRef.current) {
|
|
699
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
700
|
+
voiceSilenceTimerRef.current = null;
|
|
701
|
+
}
|
|
621
702
|
setPrompt(text);
|
|
703
|
+
liveVoiceRef.current = null;
|
|
704
|
+
setVoiceListening(false);
|
|
705
|
+
setVoiceWaitingForSilence(false);
|
|
622
706
|
},
|
|
623
707
|
onError: (error) => {
|
|
708
|
+
setVoiceWaitingForSilence(false);
|
|
624
709
|
pushMessage("error", `Voice transcription error: ${error.message}`);
|
|
625
710
|
}
|
|
626
711
|
});
|
|
627
|
-
}, [pushMessage]);
|
|
712
|
+
}, [pushMessage, stopLiveVoice]);
|
|
628
713
|
|
|
629
714
|
useEffect(() => {
|
|
630
715
|
return () => {
|
|
716
|
+
if (voiceSilenceTimerRef.current) {
|
|
717
|
+
clearTimeout(voiceSilenceTimerRef.current);
|
|
718
|
+
voiceSilenceTimerRef.current = null;
|
|
719
|
+
}
|
|
631
720
|
const controller = liveVoiceRef.current;
|
|
632
721
|
if (controller) {
|
|
633
722
|
void controller.stop();
|
|
@@ -635,6 +724,13 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
635
724
|
};
|
|
636
725
|
}, []);
|
|
637
726
|
|
|
727
|
+
useEffect(() => {
|
|
728
|
+
if (!voiceEnabled || !user || thinking || voiceListening || liveVoiceRef.current) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
startLiveVoice(false);
|
|
732
|
+
}, [startLiveVoice, thinking, user, voiceEnabled, voiceListening]);
|
|
733
|
+
|
|
638
734
|
useEffect(() => {
|
|
639
735
|
if (!trustedWorkspace) {
|
|
640
736
|
return;
|
|
@@ -652,6 +748,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
652
748
|
|
|
653
749
|
const cached = loadSession();
|
|
654
750
|
if (!cached) {
|
|
751
|
+
backend.setAuthSession(null);
|
|
655
752
|
if (!cancelled) {
|
|
656
753
|
setBooting(false);
|
|
657
754
|
}
|
|
@@ -661,8 +758,12 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
661
758
|
const valid = await backend.validateSession(cached);
|
|
662
759
|
if (!valid) {
|
|
663
760
|
clearSession();
|
|
761
|
+
backend.setAuthSession(null);
|
|
664
762
|
} else if (!cancelled) {
|
|
665
|
-
|
|
763
|
+
const hydrated = await backend.getUserProfile(cached).catch(() => cached);
|
|
764
|
+
saveSession(hydrated);
|
|
765
|
+
backend.setAuthSession(hydrated);
|
|
766
|
+
setUser(hydrated);
|
|
666
767
|
setMessages([
|
|
667
768
|
{kind: "system", text: "Welcome back. Type a message or /help."},
|
|
668
769
|
{kind: "system", text: ""}
|
|
@@ -796,7 +897,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
796
897
|
try {
|
|
797
898
|
await backend.deleteSession(selected.id);
|
|
798
899
|
setHistoryRows((prev) => prev.filter((row) => row.id !== selected.id));
|
|
799
|
-
pushMessage("
|
|
900
|
+
pushMessage("system", `Archived session: ${selected.title}`);
|
|
800
901
|
} catch (error) {
|
|
801
902
|
pushMessage("error", `Failed to delete session: ${error instanceof Error ? error.message : String(error)}`);
|
|
802
903
|
}
|
|
@@ -833,8 +934,10 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
833
934
|
}
|
|
834
935
|
|
|
835
936
|
const authSession = data as AuthSession;
|
|
836
|
-
|
|
837
|
-
|
|
937
|
+
const hydrated = await backend.getUserProfile(authSession).catch(() => authSession);
|
|
938
|
+
saveSession(hydrated);
|
|
939
|
+
backend.setAuthSession(hydrated);
|
|
940
|
+
setUser(hydrated);
|
|
838
941
|
setEmail("");
|
|
839
942
|
setPassword("");
|
|
840
943
|
setMessages([
|
|
@@ -850,8 +953,91 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
850
953
|
|
|
851
954
|
const handleEvent = useCallback(
|
|
852
955
|
async (event: AgentEvent, activeSessionId: string): Promise<string | null> => {
|
|
956
|
+
if (event.type === "file_edit_start") {
|
|
957
|
+
const data = (event.data ?? {}) as Record<string, unknown>;
|
|
958
|
+
const toolId =
|
|
959
|
+
typeof event.tool_id === "string"
|
|
960
|
+
? event.tool_id
|
|
961
|
+
: typeof data.tool_id === "string"
|
|
962
|
+
? data.tool_id
|
|
963
|
+
: "";
|
|
964
|
+
const toolName =
|
|
965
|
+
typeof event.tool_name === "string"
|
|
966
|
+
? event.tool_name
|
|
967
|
+
: typeof data.tool_name === "string"
|
|
968
|
+
? data.tool_name
|
|
969
|
+
: undefined;
|
|
970
|
+
if (toolId) {
|
|
971
|
+
const current = fileEditBuffersRef.current.get(toolId) ?? {path: "(unknown)", chunks: [] as string[], toolName: undefined};
|
|
972
|
+
fileEditBuffersRef.current.set(toolId, {...current, toolName});
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (event.type === "file_edit_path") {
|
|
978
|
+
const data = (event.data ?? {}) as Record<string, unknown>;
|
|
979
|
+
const toolId =
|
|
980
|
+
typeof event.tool_id === "string"
|
|
981
|
+
? event.tool_id
|
|
982
|
+
: typeof data.tool_id === "string"
|
|
983
|
+
? data.tool_id
|
|
984
|
+
: "";
|
|
985
|
+
const path =
|
|
986
|
+
typeof event.path === "string"
|
|
987
|
+
? event.path
|
|
988
|
+
: typeof data.path === "string"
|
|
989
|
+
? data.path
|
|
990
|
+
: "(unknown)";
|
|
991
|
+
const toolName =
|
|
992
|
+
typeof event.tool_name === "string"
|
|
993
|
+
? event.tool_name
|
|
994
|
+
: typeof data.tool_name === "string"
|
|
995
|
+
? data.tool_name
|
|
996
|
+
: undefined;
|
|
997
|
+
if (toolId) {
|
|
998
|
+
const current = fileEditBuffersRef.current.get(toolId);
|
|
999
|
+
fileEditBuffersRef.current.set(toolId, {path, chunks: [] as string[], toolName: toolName ?? current?.toolName});
|
|
1000
|
+
}
|
|
1001
|
+
if (!isSuperAdmin) {
|
|
1002
|
+
const resolvedToolName = toolName ?? fileEditBuffersRef.current.get(toolId)?.toolName;
|
|
1003
|
+
const usingBulkWriter = resolvedToolName === "bulk_file_writer";
|
|
1004
|
+
pushToolCard({
|
|
1005
|
+
kind: "fileEdit",
|
|
1006
|
+
toolName: resolvedToolName ?? "edit_file",
|
|
1007
|
+
locality: "LOCAL",
|
|
1008
|
+
summary: usingBulkWriter ? `Writing file ${path}` : `Editing ${path}`,
|
|
1009
|
+
path
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (event.type === "file_edit_delta") {
|
|
1016
|
+
const data = (event.data ?? {}) as Record<string, unknown>;
|
|
1017
|
+
const toolId =
|
|
1018
|
+
typeof event.tool_id === "string"
|
|
1019
|
+
? event.tool_id
|
|
1020
|
+
: typeof data.tool_id === "string"
|
|
1021
|
+
? data.tool_id
|
|
1022
|
+
: "";
|
|
1023
|
+
const chunk = typeof event.content === "string" ? event.content : "";
|
|
1024
|
+
if (toolId && chunk) {
|
|
1025
|
+
const current = fileEditBuffersRef.current.get(toolId) ?? {path: "(unknown)", chunks: [] as string[], toolName: undefined};
|
|
1026
|
+
current.chunks.push(chunk);
|
|
1027
|
+
fileEditBuffersRef.current.set(toolId, current);
|
|
1028
|
+
}
|
|
1029
|
+
if (isSuperAdmin) {
|
|
1030
|
+
const raw = JSON.stringify(event);
|
|
1031
|
+
pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
|
|
1032
|
+
}
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
853
1036
|
const assistantPiece = extractAssistantText(event);
|
|
854
|
-
if (
|
|
1037
|
+
if (
|
|
1038
|
+
assistantPiece &&
|
|
1039
|
+
!["tool_result", "error", "credits_update", "credits_exhausted", "file_edit_delta", "file_edit_path"].includes(event.type)
|
|
1040
|
+
) {
|
|
855
1041
|
return assistantPiece;
|
|
856
1042
|
}
|
|
857
1043
|
|
|
@@ -872,22 +1058,45 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
872
1058
|
});
|
|
873
1059
|
}
|
|
874
1060
|
|
|
875
|
-
|
|
1061
|
+
if (isSuperAdmin) {
|
|
1062
|
+
pushMessage("tool", `[LOCAL] ▸ ran: ${command.slice(0, 60)}...`);
|
|
1063
|
+
} else {
|
|
1064
|
+
pushToolCard({
|
|
1065
|
+
kind: "terminal",
|
|
1066
|
+
toolName: "run_in_terminal",
|
|
1067
|
+
locality: "LOCAL",
|
|
1068
|
+
summary: `Running command: ${command.slice(0, 80)}`
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
876
1071
|
return null;
|
|
877
1072
|
}
|
|
878
1073
|
|
|
879
1074
|
// Apply file operations to the local workspace immediately.
|
|
880
1075
|
if (event.type === "tool_result" && event.success) {
|
|
881
1076
|
const d = (event.data ?? {}) as Record<string, unknown>;
|
|
882
|
-
const resultType =
|
|
1077
|
+
const resultType = event.result_type;
|
|
1078
|
+
const toolResultId = typeof event.tool_call_id === "string" ? event.tool_call_id : "";
|
|
883
1079
|
const relPath = typeof d.path === "string" ? d.path : null;
|
|
884
1080
|
const lang = typeof d.language === "string" ? d.language : "plaintext";
|
|
1081
|
+
const locality: "LOCAL" | "REMOTE" = d.local === true ? "LOCAL" : "REMOTE";
|
|
885
1082
|
|
|
886
1083
|
if (relPath) {
|
|
887
1084
|
if (resultType === "file_create") {
|
|
888
1085
|
const content = typeof d.content === "string" ? d.content : "";
|
|
889
1086
|
writeLocalFile(relPath, content, lang);
|
|
890
|
-
|
|
1087
|
+
const snippetLines = extractSnippetLines(content);
|
|
1088
|
+
if (isSuperAdmin) {
|
|
1089
|
+
pushMessage("tool", `[LOCAL] ✓ wrote ${relPath}`);
|
|
1090
|
+
} else {
|
|
1091
|
+
pushToolCard({
|
|
1092
|
+
kind: "fileCreate",
|
|
1093
|
+
toolName: "create_file",
|
|
1094
|
+
locality,
|
|
1095
|
+
summary: `Created ${relPath}`,
|
|
1096
|
+
path: relPath,
|
|
1097
|
+
snippetLines
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
891
1100
|
} else if (resultType === "file_edit") {
|
|
892
1101
|
const content =
|
|
893
1102
|
typeof d.full_new_content === "string"
|
|
@@ -897,13 +1106,79 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
897
1106
|
: null;
|
|
898
1107
|
if (content !== null) {
|
|
899
1108
|
writeLocalFile(relPath, content, lang);
|
|
900
|
-
|
|
1109
|
+
const snippetLines = extractDiffSnippet(d.diff) ?? extractSnippetLines(content);
|
|
1110
|
+
if (isSuperAdmin) {
|
|
1111
|
+
pushMessage("tool", `[LOCAL] ✓ edited ${relPath}`);
|
|
1112
|
+
} else {
|
|
1113
|
+
const toolId = toolResultId;
|
|
1114
|
+
const streamed = toolId ? fileEditBuffersRef.current.get(toolId) : undefined;
|
|
1115
|
+
const streamedSnippet = streamed ? extractSnippetLines(streamed.chunks.join("")) : [];
|
|
1116
|
+
pushToolCard({
|
|
1117
|
+
kind: "fileEdit",
|
|
1118
|
+
toolName: "edit_file",
|
|
1119
|
+
locality,
|
|
1120
|
+
summary: `Updated ${relPath}`,
|
|
1121
|
+
path: relPath,
|
|
1122
|
+
snippetLines: streamedSnippet.length > 0 ? streamedSnippet : snippetLines
|
|
1123
|
+
});
|
|
1124
|
+
if (toolId) {
|
|
1125
|
+
fileEditBuffersRef.current.delete(toolId);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
901
1128
|
}
|
|
902
1129
|
} else if (resultType === "file_delete") {
|
|
903
1130
|
deleteLocalFile(relPath);
|
|
904
|
-
|
|
1131
|
+
if (isSuperAdmin) {
|
|
1132
|
+
pushMessage("tool", `[LOCAL] ✓ deleted ${relPath}`);
|
|
1133
|
+
} else {
|
|
1134
|
+
pushToolCard({
|
|
1135
|
+
kind: "fileDelete",
|
|
1136
|
+
toolName: "delete_file",
|
|
1137
|
+
locality,
|
|
1138
|
+
summary: `Deleted ${relPath}`,
|
|
1139
|
+
path: relPath
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
905
1142
|
}
|
|
906
1143
|
}
|
|
1144
|
+
|
|
1145
|
+
if (!isSuperAdmin && event.tool_name === "start_preview") {
|
|
1146
|
+
const message =
|
|
1147
|
+
typeof d.message === "string"
|
|
1148
|
+
? d.message
|
|
1149
|
+
: typeof d.preview_url === "string"
|
|
1150
|
+
? `Preview: ${d.preview_url}`
|
|
1151
|
+
: "Local preview started.";
|
|
1152
|
+
pushToolCard({
|
|
1153
|
+
kind: "preview",
|
|
1154
|
+
toolName: "start_preview",
|
|
1155
|
+
locality,
|
|
1156
|
+
summary: message
|
|
1157
|
+
});
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (!isSuperAdmin && event.tool_name === "capture_screenshot") {
|
|
1162
|
+
const screenshotPath = typeof d.path === "string" ? d.path : "";
|
|
1163
|
+
const targetUrl = typeof d.url === "string" ? d.url : "";
|
|
1164
|
+
const message = screenshotPath
|
|
1165
|
+
? `Screenshot saved: ${screenshotPath}`
|
|
1166
|
+
: targetUrl
|
|
1167
|
+
? `Captured screenshot: ${targetUrl}`
|
|
1168
|
+
: "Screenshot captured.";
|
|
1169
|
+
pushToolCard({
|
|
1170
|
+
kind: "preview",
|
|
1171
|
+
toolName: "capture_screenshot",
|
|
1172
|
+
locality,
|
|
1173
|
+
summary: message,
|
|
1174
|
+
...(screenshotPath ? {path: screenshotPath} : {})
|
|
1175
|
+
});
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (toolResultId) {
|
|
1180
|
+
fileEditBuffersRef.current.delete(toolResultId);
|
|
1181
|
+
}
|
|
907
1182
|
}
|
|
908
1183
|
|
|
909
1184
|
if (event.type === "credits_update") {
|
|
@@ -922,20 +1197,49 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
922
1197
|
setCreditsRemaining(0);
|
|
923
1198
|
}
|
|
924
1199
|
|
|
1200
|
+
if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (!isSuperAdmin && event.type === "tool_start") {
|
|
1205
|
+
const tool = event.tool as {name?: string} | undefined;
|
|
1206
|
+
const name = tool?.name ?? "tool";
|
|
1207
|
+
pushToolCard({
|
|
1208
|
+
kind: "start",
|
|
1209
|
+
toolName: name,
|
|
1210
|
+
locality: "REMOTE",
|
|
1211
|
+
summary: `${name} is running...`
|
|
1212
|
+
});
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
925
1216
|
const toolLine = eventToToolLine(event);
|
|
926
1217
|
if (toolLine) {
|
|
927
1218
|
if (event.type === "error" || event.type === "credits_exhausted") {
|
|
928
1219
|
pushMessage("error", toolLine);
|
|
929
1220
|
} else {
|
|
930
|
-
|
|
1221
|
+
if (isSuperAdmin) {
|
|
1222
|
+
pushMessage("tool", toolLine);
|
|
1223
|
+
} else if (event.type === "tool_result") {
|
|
1224
|
+
const mark = event.success ? "completed" : "failed";
|
|
1225
|
+
const toolName = typeof event.tool_name === "string" ? event.tool_name : "tool";
|
|
1226
|
+
pushToolCard({
|
|
1227
|
+
kind: event.success ? "success" : "error",
|
|
1228
|
+
toolName,
|
|
1229
|
+
locality: ((event.data ?? {}) as Record<string, unknown>).local === true ? "LOCAL" : "REMOTE",
|
|
1230
|
+
summary: `${toolName} ${mark}`
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
931
1233
|
}
|
|
932
1234
|
} else if (event.type !== "thinking") {
|
|
933
|
-
|
|
934
|
-
|
|
1235
|
+
if (isSuperAdmin) {
|
|
1236
|
+
const raw = JSON.stringify(event);
|
|
1237
|
+
pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
|
|
1238
|
+
}
|
|
935
1239
|
}
|
|
936
1240
|
return null;
|
|
937
1241
|
},
|
|
938
|
-
[backend, deleteLocalFile, pushMessage, writeLocalFile]
|
|
1242
|
+
[backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]
|
|
939
1243
|
);
|
|
940
1244
|
|
|
941
1245
|
const sendPrompt = useCallback(
|
|
@@ -948,7 +1252,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
948
1252
|
if (text === "/help") {
|
|
949
1253
|
pushMessage(
|
|
950
1254
|
"system",
|
|
951
|
-
"/new /history /voice on|off|status
|
|
1255
|
+
"/new /history /voice /voice on|off|status /settings /settings model <id> /logout /exit"
|
|
952
1256
|
);
|
|
953
1257
|
pushMessage("system", "");
|
|
954
1258
|
return;
|
|
@@ -956,7 +1260,20 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
956
1260
|
if (text === "/settings") {
|
|
957
1261
|
pushMessage(
|
|
958
1262
|
"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()}`
|
|
1263
|
+
`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()}`
|
|
1264
|
+
);
|
|
1265
|
+
pushMessage("system", "");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (text === "/voice") {
|
|
1269
|
+
if (!voiceEnabled) {
|
|
1270
|
+
setVoiceEnabled(true);
|
|
1271
|
+
startLiveVoice(true);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
pushMessage(
|
|
1275
|
+
"system",
|
|
1276
|
+
`Voice input is on${voiceListening ? " (currently listening)" : ""}. Use /voice off to disable.`
|
|
960
1277
|
);
|
|
961
1278
|
pushMessage("system", "");
|
|
962
1279
|
return;
|
|
@@ -984,39 +1301,22 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
984
1301
|
if (text === "/voice status") {
|
|
985
1302
|
pushMessage(
|
|
986
1303
|
"system",
|
|
987
|
-
`Voice
|
|
1304
|
+
`Voice input is ${voiceEnabled ? "on" : "off"}${voiceListening ? " (live transcription active)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`
|
|
988
1305
|
);
|
|
989
1306
|
pushMessage("system", "");
|
|
990
1307
|
return;
|
|
991
1308
|
}
|
|
992
1309
|
if (text === "/voice on") {
|
|
993
1310
|
setVoiceEnabled(true);
|
|
994
|
-
|
|
1311
|
+
startLiveVoice(true);
|
|
1312
|
+
pushMessage("system", `Voice input enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
|
|
995
1313
|
pushMessage("system", "");
|
|
996
1314
|
return;
|
|
997
1315
|
}
|
|
998
1316
|
if (text === "/voice off") {
|
|
999
1317
|
await stopLiveVoice();
|
|
1000
1318
|
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.");
|
|
1319
|
+
pushMessage("system", "Voice input disabled.");
|
|
1020
1320
|
pushMessage("system", "");
|
|
1021
1321
|
return;
|
|
1022
1322
|
}
|
|
@@ -1025,13 +1325,11 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1025
1325
|
if (!transcribed) {
|
|
1026
1326
|
pushMessage(
|
|
1027
1327
|
"error",
|
|
1028
|
-
"No speech transcribed.
|
|
1328
|
+
"No speech transcribed. Ensure you're signed in and that mic capture works (optional ASTRA_STT_CAPTURE_COMMAND)."
|
|
1029
1329
|
);
|
|
1030
1330
|
return;
|
|
1031
1331
|
}
|
|
1032
|
-
|
|
1033
|
-
setPrompt(transcribed);
|
|
1034
|
-
pushMessage("system", "Transcribed input ready. Press Enter to send.");
|
|
1332
|
+
setVoiceQueuedPrompt(transcribed.trim());
|
|
1035
1333
|
return;
|
|
1036
1334
|
}
|
|
1037
1335
|
if (text === "/new") {
|
|
@@ -1045,6 +1343,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1045
1343
|
if (text === "/logout") {
|
|
1046
1344
|
await stopLiveVoice();
|
|
1047
1345
|
clearSession();
|
|
1346
|
+
backend.setAuthSession(null);
|
|
1048
1347
|
setUser(null);
|
|
1049
1348
|
setMessages([]);
|
|
1050
1349
|
setChatMessages([]);
|
|
@@ -1113,9 +1412,6 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1113
1412
|
? `Remote result (not yet confirmed as local filesystem change): ${cleanedAssistant}`
|
|
1114
1413
|
: cleanedAssistant;
|
|
1115
1414
|
pushMessage("assistant", guardedAssistant);
|
|
1116
|
-
if (voiceEnabled) {
|
|
1117
|
-
speakText(guardedAssistant);
|
|
1118
|
-
}
|
|
1119
1415
|
setChatMessages((prev) => [...prev, {role: "assistant", content: cleanedAssistant}]);
|
|
1120
1416
|
} else {
|
|
1121
1417
|
setChatMessages((prev) => [...prev, {role: "assistant", content: assistant}]);
|
|
@@ -1143,10 +1439,25 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1143
1439
|
user,
|
|
1144
1440
|
voiceEnabled,
|
|
1145
1441
|
voiceListening,
|
|
1442
|
+
voiceWaitingForSilence,
|
|
1146
1443
|
workspaceRoot
|
|
1147
1444
|
]
|
|
1148
1445
|
);
|
|
1149
1446
|
|
|
1447
|
+
useEffect(() => {
|
|
1448
|
+
if (!voiceQueuedPrompt || !user || thinking) {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
const queued = voiceQueuedPrompt.trim();
|
|
1452
|
+
setVoiceQueuedPrompt(null);
|
|
1453
|
+
if (!queued) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
pushMessage("system", `Voice input: ${queued}`);
|
|
1457
|
+
setPrompt("");
|
|
1458
|
+
void sendPrompt(queued);
|
|
1459
|
+
}, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
|
|
1460
|
+
|
|
1150
1461
|
if (!trustedWorkspace) {
|
|
1151
1462
|
return (
|
|
1152
1463
|
<Box flexDirection="column">
|
|
@@ -1363,12 +1674,12 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1363
1674
|
<Box flexDirection="row">
|
|
1364
1675
|
<Text color="#4a6070">voice </Text>
|
|
1365
1676
|
<Text color={voiceEnabled ? "#9ad5ff" : "#5a7a9a"}>
|
|
1366
|
-
{voiceEnabled ? (voiceListening ? "on/listening" : "on") : "off"}
|
|
1677
|
+
{voiceEnabled ? (voiceListening ? (voiceWaitingForSilence ? "on/waiting" : "on/listening") : "on") : "off"}
|
|
1367
1678
|
</Text>
|
|
1368
1679
|
</Box>
|
|
1369
1680
|
</Box>
|
|
1370
1681
|
<Text color="#2a3a50">{DIVIDER}</Text>
|
|
1371
|
-
<Text color="#3a5068">/help /new /history /voice on|off|status
|
|
1682
|
+
<Text color="#3a5068">/help /new /history /voice /voice on|off|status /settings /logout /exit</Text>
|
|
1372
1683
|
<Text color="#2a3a50">{DIVIDER}</Text>
|
|
1373
1684
|
<Box flexDirection="column" marginTop={1}>
|
|
1374
1685
|
{messages.map((message, index) => {
|
|
@@ -1378,6 +1689,55 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1378
1689
|
if (isSpacing) {
|
|
1379
1690
|
return <Box key={`${index}-spacer`} marginTop={0}><Text> </Text></Box>;
|
|
1380
1691
|
}
|
|
1692
|
+
|
|
1693
|
+
if (message.kind === "tool" && message.card && !isSuperAdmin) {
|
|
1694
|
+
const card = message.card;
|
|
1695
|
+
const icon =
|
|
1696
|
+
card.kind === "error"
|
|
1697
|
+
? "✕"
|
|
1698
|
+
: card.kind === "fileDelete"
|
|
1699
|
+
? "🗑"
|
|
1700
|
+
: card.kind === "fileCreate"
|
|
1701
|
+
? "+"
|
|
1702
|
+
: card.kind === "fileEdit"
|
|
1703
|
+
? "✎"
|
|
1704
|
+
: card.kind === "preview"
|
|
1705
|
+
? "◉"
|
|
1706
|
+
: card.kind === "terminal"
|
|
1707
|
+
? "⌘"
|
|
1708
|
+
: card.kind === "start"
|
|
1709
|
+
? "…"
|
|
1710
|
+
: "✓";
|
|
1711
|
+
const accent =
|
|
1712
|
+
card.kind === "error"
|
|
1713
|
+
? "#ff8d8d"
|
|
1714
|
+
: card.kind === "fileDelete"
|
|
1715
|
+
? "#8a96a6"
|
|
1716
|
+
: card.kind === "fileEdit"
|
|
1717
|
+
? "#b7d4ff"
|
|
1718
|
+
: card.kind === "fileCreate"
|
|
1719
|
+
? "#a5e9c5"
|
|
1720
|
+
: card.kind === "preview"
|
|
1721
|
+
? "#9ad5ff"
|
|
1722
|
+
: "#9bc5ff";
|
|
1723
|
+
return (
|
|
1724
|
+
<Box key={`${index}-${message.kind}`} flexDirection="row">
|
|
1725
|
+
<Text color={style.labelColor}>{paddedLabel}</Text>
|
|
1726
|
+
<Box flexDirection="column">
|
|
1727
|
+
<Text color={accent}>
|
|
1728
|
+
{icon} {card.summary} <Text color="#5a7a9a">[{card.locality}]</Text>
|
|
1729
|
+
</Text>
|
|
1730
|
+
{card.path ? <Text color="#6c88a8">path: {card.path}</Text> : null}
|
|
1731
|
+
{(card.snippetLines ?? []).slice(0, TOOL_SNIPPET_LINES).map((line, idx) => (
|
|
1732
|
+
<Text key={`${index}-snippet-${idx}`} color="#8ea1bd">
|
|
1733
|
+
{line}
|
|
1734
|
+
</Text>
|
|
1735
|
+
))}
|
|
1736
|
+
</Box>
|
|
1737
|
+
</Box>
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1381
1741
|
return (
|
|
1382
1742
|
<Box key={`${index}-${message.kind}`} flexDirection="row">
|
|
1383
1743
|
<Text color={style.labelColor} bold={style.bold}>
|
|
@@ -1412,16 +1772,32 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1412
1772
|
</Text>
|
|
1413
1773
|
</Box>
|
|
1414
1774
|
) : null}
|
|
1775
|
+
{voiceEnabled && !thinking ? (
|
|
1776
|
+
<Box flexDirection="row" marginTop={1}>
|
|
1777
|
+
<Text color="#9ad5ff">{"🎤 voice".padEnd(LABEL_WIDTH, " ")}</Text>
|
|
1778
|
+
{voiceListening && !voiceWaitingForSilence ? (
|
|
1779
|
+
<Text color="#a6d9ff">🎙 listening... goblin ears activated 👂</Text>
|
|
1780
|
+
) : voiceWaitingForSilence ? (
|
|
1781
|
+
<Text color="#b7c4d8">⏸ waiting for silence... dramatic pause loading...</Text>
|
|
1782
|
+
) : (
|
|
1783
|
+
<Text color="#6f8199">voice armed... say something when ready</Text>
|
|
1784
|
+
)}
|
|
1785
|
+
</Box>
|
|
1786
|
+
) : null}
|
|
1415
1787
|
<Box marginTop={1} flexDirection="row">
|
|
1416
1788
|
<Text color="#7aa2ff">❯ </Text>
|
|
1417
1789
|
<TextInput
|
|
1418
1790
|
value={prompt}
|
|
1419
|
-
onChange={setPrompt}
|
|
1420
1791
|
onSubmit={(value) => {
|
|
1421
1792
|
setPrompt("");
|
|
1422
1793
|
void sendPrompt(value);
|
|
1423
1794
|
}}
|
|
1424
|
-
|
|
1795
|
+
onChange={(value) => {
|
|
1796
|
+
if (!voiceListening) {
|
|
1797
|
+
setPrompt(value);
|
|
1798
|
+
}
|
|
1799
|
+
}}
|
|
1800
|
+
placeholder={voiceEnabled ? "Ask Astra... (voice on: auto listen + send on silence)" : "Ask Astra..."}
|
|
1425
1801
|
/>
|
|
1426
1802
|
</Box>
|
|
1427
1803
|
</Box>
|