@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 CHANGED
@@ -13,7 +13,9 @@ ASTRA_STT_COMMAND=
13
13
  # Optional: live Whisper transcription tuning
14
14
  ASTRA_STT_MODEL=whisper-1
15
15
  ASTRA_STT_CHUNK_SECONDS=2.5
16
+ ASTRA_VOICE_SILENCE_MS=3000
16
17
  # Optional override for microphone capture.
17
18
  # Use placeholders {output} and {seconds}.
18
- # Example (macOS): sox -q -d -r 16000 -c 1 -b 16 "{output}" trim 0 {seconds}
19
+ # Default on macOS prefers ffmpeg; custom example:
20
+ # ffmpeg -f avfoundation -i ":0" -ar 16000 -ac 1 -t {seconds} "{output}"
19
21
  ASTRA_STT_CAPTURE_COMMAND=
package/README.md CHANGED
@@ -57,7 +57,7 @@ No-args mode launches the Ink terminal UI:
57
57
  - slash commands:
58
58
  - `/help`, `/new`, `/settings`, `/logout`, `/exit`
59
59
  - `/history` (open searchable session history panel)
60
- - `/voice on|off|status|start|stop|input`
60
+ - `/voice`, `/voice on|off|status`, `/voice input` (user voice input; auto-send after silence)
61
61
  - tool-event rendering (`tool_start`, `tool_result`, `credits_update`, `error`)
62
62
  - terminal bridge auto-run for `run_in_terminal` + callback to `/api/agent/terminal-result`
63
63
 
package/dist/app/App.js CHANGED
@@ -12,7 +12,7 @@ import { getBackendUrl, getDefaultClientId, getDefaultModel, getProviderForModel
12
12
  import { runTerminalCommand } from "../lib/terminalBridge.js";
13
13
  import { isWorkspaceTrusted, trustWorkspace } from "../lib/trustStore.js";
14
14
  import { scanWorkspace } from "../lib/workspaceScanner.js";
15
- import { speakText, startLiveTranscription, transcribeOnce } from "../lib/voice.js";
15
+ import { startLiveTranscription, transcribeOnce } from "../lib/voice.js";
16
16
  // const ASTRA_ASCII = `
17
17
  // █████╗ ███████╗████████╗██████╗ █████╗ ██████╗ ██████╗ ██████╗ ███████╗
18
18
  // ██╔══██╗██╔════╝╚══██╔══╝██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
@@ -43,9 +43,21 @@ const centerLine = (text, width = WELCOME_WIDTH) => {
43
43
  };
44
44
  const FOUNDER_WELCOME = centerLine("Welcome to Astra from Astra CEO & Founder, Sean Donovan");
45
45
  const HISTORY_SETTINGS_URL = "https://astra-web-builder.vercel.app/settings";
46
+ const VOICE_SILENCE_MS = Number(process.env.ASTRA_VOICE_SILENCE_MS ?? "3000");
47
+ const TOOL_SNIPPET_LINES = 6;
48
+ const NOISY_EVENT_TYPES = new Set([
49
+ "timing",
50
+ "session_info",
51
+ "axon_status",
52
+ "orchestration_plan",
53
+ "task_plan",
54
+ "task_update",
55
+ "preset_status"
56
+ ]);
46
57
  const eventToToolLine = (event) => {
47
58
  if (event.type === "tool_start") {
48
- const name = event.tool?.name ?? "tool";
59
+ const tool = event.tool;
60
+ const name = tool?.name ?? "tool";
49
61
  return `↳ ${name} executing...`;
50
62
  }
51
63
  if (event.type === "tool_result") {
@@ -70,6 +82,26 @@ const eventToToolLine = (event) => {
70
82
  }
71
83
  return null;
72
84
  };
85
+ const extractSnippetLines = (content, maxLines = TOOL_SNIPPET_LINES) => {
86
+ if (!content) {
87
+ return [];
88
+ }
89
+ const lines = content.split("\n").map((line) => line.trimEnd());
90
+ if (lines.length <= maxLines) {
91
+ return lines;
92
+ }
93
+ return lines.slice(0, maxLines);
94
+ };
95
+ const extractDiffSnippet = (diff, maxLines = TOOL_SNIPPET_LINES) => {
96
+ if (!Array.isArray(diff)) {
97
+ return [];
98
+ }
99
+ const interesting = diff
100
+ .map((line) => String(line))
101
+ .filter((line) => line.startsWith("+") || line.startsWith("-"))
102
+ .map((line) => line.slice(0, 220));
103
+ return interesting.slice(0, maxLines);
104
+ };
73
105
  const looksLikeLocalFilesystemClaim = (text) => {
74
106
  const lower = text.toLowerCase();
75
107
  const changeWord = lower.includes("created") ||
@@ -365,6 +397,8 @@ export const AstraApp = () => {
365
397
  const [streamingText, setStreamingText] = useState("");
366
398
  const [voiceEnabled, setVoiceEnabled] = useState(false);
367
399
  const [voiceListening, setVoiceListening] = useState(false);
400
+ const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
401
+ const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState(null);
368
402
  const [historyOpen, setHistoryOpen] = useState(false);
369
403
  const [historyMode, setHistoryMode] = useState("picker");
370
404
  const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
@@ -373,9 +407,15 @@ export const AstraApp = () => {
373
407
  const [historyRows, setHistoryRows] = useState([]);
374
408
  const [historyIndex, setHistoryIndex] = useState(0);
375
409
  const liveVoiceRef = useRef(null);
410
+ const voiceSilenceTimerRef = useRef(null);
411
+ const fileEditBuffersRef = useRef(new Map());
412
+ const isSuperAdmin = user?.role === "super_admin";
376
413
  const pushMessage = useCallback((kind, text) => {
377
414
  setMessages((prev) => [...prev, { kind, text }].slice(-300));
378
415
  }, []);
416
+ const pushToolCard = useCallback((card) => {
417
+ setMessages((prev) => [...prev, { kind: "tool", text: card.summary, card }].slice(-300));
418
+ }, []);
379
419
  const filteredHistory = useMemo(() => {
380
420
  const q = historyQuery.trim().toLowerCase();
381
421
  if (!q) {
@@ -439,41 +479,80 @@ export const AstraApp = () => {
439
479
  }
440
480
  }, [backend, pushMessage, user]);
441
481
  const stopLiveVoice = useCallback(async () => {
482
+ if (voiceSilenceTimerRef.current) {
483
+ clearTimeout(voiceSilenceTimerRef.current);
484
+ voiceSilenceTimerRef.current = null;
485
+ }
442
486
  const controller = liveVoiceRef.current;
443
487
  if (!controller) {
488
+ setVoiceWaitingForSilence(false);
444
489
  return;
445
490
  }
446
491
  liveVoiceRef.current = null;
447
492
  await controller.stop();
448
493
  setVoiceListening(false);
494
+ setVoiceWaitingForSilence(false);
449
495
  }, []);
450
- const startLiveVoice = useCallback(() => {
496
+ const startLiveVoice = useCallback((announce = true) => {
451
497
  if (liveVoiceRef.current) {
452
498
  return;
453
499
  }
454
500
  setVoiceEnabled(true);
455
501
  setVoiceListening(true);
456
- pushMessage("system", "Live transcription started. Speak now…");
502
+ setVoiceWaitingForSilence(false);
503
+ if (announce) {
504
+ pushMessage("system", "Voice input started. Speak now…");
505
+ }
457
506
  liveVoiceRef.current = startLiveTranscription({
458
507
  onPartial: (text) => {
459
508
  setPrompt(text);
509
+ if (voiceSilenceTimerRef.current) {
510
+ clearTimeout(voiceSilenceTimerRef.current);
511
+ }
512
+ const candidate = text.trim();
513
+ if (!candidate) {
514
+ return;
515
+ }
516
+ setVoiceWaitingForSilence(true);
517
+ voiceSilenceTimerRef.current = setTimeout(() => {
518
+ setVoiceQueuedPrompt(candidate);
519
+ void stopLiveVoice();
520
+ }, VOICE_SILENCE_MS);
460
521
  },
461
522
  onFinal: (text) => {
523
+ if (voiceSilenceTimerRef.current) {
524
+ clearTimeout(voiceSilenceTimerRef.current);
525
+ voiceSilenceTimerRef.current = null;
526
+ }
462
527
  setPrompt(text);
528
+ liveVoiceRef.current = null;
529
+ setVoiceListening(false);
530
+ setVoiceWaitingForSilence(false);
463
531
  },
464
532
  onError: (error) => {
533
+ setVoiceWaitingForSilence(false);
465
534
  pushMessage("error", `Voice transcription error: ${error.message}`);
466
535
  }
467
536
  });
468
- }, [pushMessage]);
537
+ }, [pushMessage, stopLiveVoice]);
469
538
  useEffect(() => {
470
539
  return () => {
540
+ if (voiceSilenceTimerRef.current) {
541
+ clearTimeout(voiceSilenceTimerRef.current);
542
+ voiceSilenceTimerRef.current = null;
543
+ }
471
544
  const controller = liveVoiceRef.current;
472
545
  if (controller) {
473
546
  void controller.stop();
474
547
  }
475
548
  };
476
549
  }, []);
550
+ useEffect(() => {
551
+ if (!voiceEnabled || !user || thinking || voiceListening || liveVoiceRef.current) {
552
+ return;
553
+ }
554
+ startLiveVoice(false);
555
+ }, [startLiveVoice, thinking, user, voiceEnabled, voiceListening]);
477
556
  useEffect(() => {
478
557
  if (!trustedWorkspace) {
479
558
  return;
@@ -490,6 +569,7 @@ export const AstraApp = () => {
490
569
  }
491
570
  const cached = loadSession();
492
571
  if (!cached) {
572
+ backend.setAuthSession(null);
493
573
  if (!cancelled) {
494
574
  setBooting(false);
495
575
  }
@@ -498,9 +578,13 @@ export const AstraApp = () => {
498
578
  const valid = await backend.validateSession(cached);
499
579
  if (!valid) {
500
580
  clearSession();
581
+ backend.setAuthSession(null);
501
582
  }
502
583
  else if (!cancelled) {
503
- setUser(cached);
584
+ const hydrated = await backend.getUserProfile(cached).catch(() => cached);
585
+ saveSession(hydrated);
586
+ backend.setAuthSession(hydrated);
587
+ setUser(hydrated);
504
588
  setMessages([
505
589
  { kind: "system", text: "Welcome back. Type a message or /help." },
506
590
  { kind: "system", text: "" }
@@ -662,8 +746,10 @@ export const AstraApp = () => {
662
746
  throw new Error(data.error);
663
747
  }
664
748
  const authSession = data;
665
- saveSession(authSession);
666
- setUser(authSession);
749
+ const hydrated = await backend.getUserProfile(authSession).catch(() => authSession);
750
+ saveSession(hydrated);
751
+ backend.setAuthSession(hydrated);
752
+ setUser(hydrated);
667
753
  setEmail("");
668
754
  setPassword("");
669
755
  setMessages([
@@ -679,8 +765,42 @@ export const AstraApp = () => {
679
765
  }
680
766
  }, [backend, email, loginMode, password]);
681
767
  const handleEvent = useCallback(async (event, activeSessionId) => {
768
+ if (event.type === "file_edit_path") {
769
+ const data = (event.data ?? {});
770
+ const toolId = typeof data.tool_id === "string" ? data.tool_id : "";
771
+ const path = typeof data.path === "string" ? data.path : "(unknown)";
772
+ if (toolId) {
773
+ fileEditBuffersRef.current.set(toolId, { path, chunks: [] });
774
+ }
775
+ if (!isSuperAdmin) {
776
+ pushToolCard({
777
+ kind: "fileEdit",
778
+ toolName: "edit_file",
779
+ locality: "LOCAL",
780
+ summary: `Editing ${path}`,
781
+ path
782
+ });
783
+ }
784
+ return null;
785
+ }
786
+ if (event.type === "file_edit_delta") {
787
+ const data = (event.data ?? {});
788
+ const toolId = typeof data.tool_id === "string" ? data.tool_id : "";
789
+ const chunk = typeof event.content === "string" ? event.content : "";
790
+ if (toolId && chunk) {
791
+ const current = fileEditBuffersRef.current.get(toolId) ?? { path: "(unknown)", chunks: [] };
792
+ current.chunks.push(chunk);
793
+ fileEditBuffersRef.current.set(toolId, current);
794
+ }
795
+ if (isSuperAdmin) {
796
+ const raw = JSON.stringify(event);
797
+ pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
798
+ }
799
+ return null;
800
+ }
682
801
  const assistantPiece = extractAssistantText(event);
683
- if (assistantPiece && !["tool_result", "error", "credits_update", "credits_exhausted"].includes(event.type)) {
802
+ if (assistantPiece &&
803
+ !["tool_result", "error", "credits_update", "credits_exhausted", "file_edit_delta", "file_edit_path"].includes(event.type)) {
684
804
  return assistantPiece;
685
805
  }
686
806
  if (event.type === "run_in_terminal") {
@@ -698,7 +818,17 @@ export const AstraApp = () => {
698
818
  cancelled: result.cancelled
699
819
  });
700
820
  }
701
- pushMessage("tool", `[LOCAL] ▸ ran: ${command.slice(0, 60)}...`);
821
+ if (isSuperAdmin) {
822
+ pushMessage("tool", `[LOCAL] ▸ ran: ${command.slice(0, 60)}...`);
823
+ }
824
+ else {
825
+ pushToolCard({
826
+ kind: "terminal",
827
+ toolName: "run_in_terminal",
828
+ locality: "LOCAL",
829
+ summary: `Running command: ${command.slice(0, 80)}`
830
+ });
831
+ }
702
832
  return null;
703
833
  }
704
834
  // Apply file operations to the local workspace immediately.
@@ -707,11 +837,25 @@ export const AstraApp = () => {
707
837
  const resultType = event.result_type;
708
838
  const relPath = typeof d.path === "string" ? d.path : null;
709
839
  const lang = typeof d.language === "string" ? d.language : "plaintext";
840
+ const locality = d.local === true ? "LOCAL" : "REMOTE";
710
841
  if (relPath) {
711
842
  if (resultType === "file_create") {
712
843
  const content = typeof d.content === "string" ? d.content : "";
713
844
  writeLocalFile(relPath, content, lang);
714
- pushMessage("tool", `[LOCAL] wrote ${relPath}`);
845
+ const snippetLines = extractSnippetLines(content);
846
+ if (isSuperAdmin) {
847
+ pushMessage("tool", `[LOCAL] ✓ wrote ${relPath}`);
848
+ }
849
+ else {
850
+ pushToolCard({
851
+ kind: "fileCreate",
852
+ toolName: "create_file",
853
+ locality,
854
+ summary: `Created ${relPath}`,
855
+ path: relPath,
856
+ snippetLines
857
+ });
858
+ }
715
859
  }
716
860
  else if (resultType === "file_edit") {
717
861
  const content = typeof d.full_new_content === "string"
@@ -721,14 +865,58 @@ export const AstraApp = () => {
721
865
  : null;
722
866
  if (content !== null) {
723
867
  writeLocalFile(relPath, content, lang);
724
- pushMessage("tool", `[LOCAL] edited ${relPath}`);
868
+ const snippetLines = extractDiffSnippet(d.diff) ?? extractSnippetLines(content);
869
+ if (isSuperAdmin) {
870
+ pushMessage("tool", `[LOCAL] ✓ edited ${relPath}`);
871
+ }
872
+ else {
873
+ const toolId = typeof event.tool_call_id === "string" ? event.tool_call_id : "";
874
+ const streamed = toolId ? fileEditBuffersRef.current.get(toolId) : undefined;
875
+ const streamedSnippet = streamed ? extractSnippetLines(streamed.chunks.join("")) : [];
876
+ pushToolCard({
877
+ kind: "fileEdit",
878
+ toolName: "edit_file",
879
+ locality,
880
+ summary: `Updated ${relPath}`,
881
+ path: relPath,
882
+ snippetLines: streamedSnippet.length > 0 ? streamedSnippet : snippetLines
883
+ });
884
+ if (toolId) {
885
+ fileEditBuffersRef.current.delete(toolId);
886
+ }
887
+ }
725
888
  }
726
889
  }
727
890
  else if (resultType === "file_delete") {
728
891
  deleteLocalFile(relPath);
729
- pushMessage("tool", `[LOCAL] ✓ deleted ${relPath}`);
892
+ if (isSuperAdmin) {
893
+ pushMessage("tool", `[LOCAL] ✓ deleted ${relPath}`);
894
+ }
895
+ else {
896
+ pushToolCard({
897
+ kind: "fileDelete",
898
+ toolName: "delete_file",
899
+ locality,
900
+ summary: `Deleted ${relPath}`,
901
+ path: relPath
902
+ });
903
+ }
730
904
  }
731
905
  }
906
+ if (!isSuperAdmin && event.tool_name === "start_preview") {
907
+ const message = typeof d.message === "string"
908
+ ? d.message
909
+ : typeof d.preview_url === "string"
910
+ ? `Preview: ${d.preview_url}`
911
+ : "Local preview started.";
912
+ pushToolCard({
913
+ kind: "preview",
914
+ toolName: "start_preview",
915
+ locality,
916
+ summary: message
917
+ });
918
+ return null;
919
+ }
732
920
  }
733
921
  if (event.type === "credits_update") {
734
922
  const remaining = Number(event.remaining ?? 0);
@@ -744,33 +932,71 @@ export const AstraApp = () => {
744
932
  if (event.type === "credits_exhausted") {
745
933
  setCreditsRemaining(0);
746
934
  }
935
+ if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
936
+ return null;
937
+ }
938
+ if (!isSuperAdmin && event.type === "tool_start") {
939
+ const tool = event.tool;
940
+ const name = tool?.name ?? "tool";
941
+ pushToolCard({
942
+ kind: "start",
943
+ toolName: name,
944
+ locality: "REMOTE",
945
+ summary: `${name} is running...`
946
+ });
947
+ return null;
948
+ }
747
949
  const toolLine = eventToToolLine(event);
748
950
  if (toolLine) {
749
951
  if (event.type === "error" || event.type === "credits_exhausted") {
750
952
  pushMessage("error", toolLine);
751
953
  }
752
954
  else {
753
- pushMessage("tool", toolLine);
955
+ if (isSuperAdmin) {
956
+ pushMessage("tool", toolLine);
957
+ }
958
+ else if (event.type === "tool_result") {
959
+ const mark = event.success ? "completed" : "failed";
960
+ const toolName = typeof event.tool_name === "string" ? event.tool_name : "tool";
961
+ pushToolCard({
962
+ kind: event.success ? "success" : "error",
963
+ toolName,
964
+ locality: (event.data ?? {}).local === true ? "LOCAL" : "REMOTE",
965
+ summary: `${toolName} ${mark}`
966
+ });
967
+ }
754
968
  }
755
969
  }
756
970
  else if (event.type !== "thinking") {
757
- const raw = JSON.stringify(event);
758
- pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
971
+ if (isSuperAdmin) {
972
+ const raw = JSON.stringify(event);
973
+ pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
974
+ }
759
975
  }
760
976
  return null;
761
- }, [backend, deleteLocalFile, pushMessage, writeLocalFile]);
977
+ }, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]);
762
978
  const sendPrompt = useCallback(async (rawPrompt) => {
763
979
  const text = rawPrompt.trim();
764
980
  if (!text || !user || thinking) {
765
981
  return;
766
982
  }
767
983
  if (text === "/help") {
768
- pushMessage("system", "/new /history /voice on|off|status|start|stop|input /settings /settings model <id> /logout /exit");
984
+ pushMessage("system", "/new /history /voice /voice on|off|status /settings /settings model <id> /logout /exit");
769
985
  pushMessage("system", "");
770
986
  return;
771
987
  }
772
988
  if (text === "/settings") {
773
- pushMessage("system", `Settings: mode=${runtimeMode} scope=${workspaceRoot} model=${activeModel} provider=${getProviderForModel(activeModel)} voice=${voiceEnabled ? "on" : "off"} listening=${voiceListening ? "yes" : "no"} client_id=${getDefaultClientId()} backend=${getBackendUrl()}`);
989
+ pushMessage("system", `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()}`);
990
+ pushMessage("system", "");
991
+ return;
992
+ }
993
+ if (text === "/voice") {
994
+ if (!voiceEnabled) {
995
+ setVoiceEnabled(true);
996
+ startLiveVoice(true);
997
+ return;
998
+ }
999
+ pushMessage("system", `Voice input is on${voiceListening ? " (currently listening)" : ""}. Use /voice off to disable.`);
774
1000
  pushMessage("system", "");
775
1001
  return;
776
1002
  }
@@ -792,50 +1018,31 @@ export const AstraApp = () => {
792
1018
  return;
793
1019
  }
794
1020
  if (text === "/voice status") {
795
- pushMessage("system", `Voice mode is ${voiceEnabled ? "on" : "off"}${voiceListening ? " (live transcription active)" : ""}.`);
1021
+ pushMessage("system", `Voice input is ${voiceEnabled ? "on" : "off"}${voiceListening ? " (live transcription active)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`);
796
1022
  pushMessage("system", "");
797
1023
  return;
798
1024
  }
799
1025
  if (text === "/voice on") {
800
1026
  setVoiceEnabled(true);
801
- pushMessage("system", "Voice mode enabled (assistant responses will be spoken).");
1027
+ startLiveVoice(true);
1028
+ pushMessage("system", `Voice input enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
802
1029
  pushMessage("system", "");
803
1030
  return;
804
1031
  }
805
1032
  if (text === "/voice off") {
806
1033
  await stopLiveVoice();
807
1034
  setVoiceEnabled(false);
808
- pushMessage("system", "Voice mode disabled.");
809
- pushMessage("system", "");
810
- return;
811
- }
812
- if (text === "/voice start") {
813
- if (voiceListening) {
814
- pushMessage("system", "Live transcription is already running.");
815
- return;
816
- }
817
- startLiveVoice();
818
- return;
819
- }
820
- if (text === "/voice stop") {
821
- if (!voiceListening) {
822
- pushMessage("system", "Live transcription is not running.");
823
- return;
824
- }
825
- await stopLiveVoice();
826
- pushMessage("system", "Live transcription stopped. Press Enter to send transcript.");
1035
+ pushMessage("system", "Voice input disabled.");
827
1036
  pushMessage("system", "");
828
1037
  return;
829
1038
  }
830
1039
  if (text === "/voice input") {
831
1040
  const transcribed = await transcribeOnce();
832
1041
  if (!transcribed) {
833
- pushMessage("error", "No speech transcribed. Set ASTRA_STT_COMMAND to a command that prints transcript text to stdout.");
1042
+ pushMessage("error", "No speech transcribed. Check OPENAI_API_KEY and mic capture command (optional ASTRA_STT_CAPTURE_COMMAND).");
834
1043
  return;
835
1044
  }
836
- pushMessage("tool", `[LOCAL] 🎙 ${transcribed}`);
837
- setPrompt(transcribed);
838
- pushMessage("system", "Transcribed input ready. Press Enter to send.");
1045
+ setVoiceQueuedPrompt(transcribed.trim());
839
1046
  return;
840
1047
  }
841
1048
  if (text === "/new") {
@@ -849,6 +1056,7 @@ export const AstraApp = () => {
849
1056
  if (text === "/logout") {
850
1057
  await stopLiveVoice();
851
1058
  clearSession();
1059
+ backend.setAuthSession(null);
852
1060
  setUser(null);
853
1061
  setMessages([]);
854
1062
  setChatMessages([]);
@@ -911,9 +1119,6 @@ export const AstraApp = () => {
911
1119
  ? `Remote result (not yet confirmed as local filesystem change): ${cleanedAssistant}`
912
1120
  : cleanedAssistant;
913
1121
  pushMessage("assistant", guardedAssistant);
914
- if (voiceEnabled) {
915
- speakText(guardedAssistant);
916
- }
917
1122
  setChatMessages((prev) => [...prev, { role: "assistant", content: cleanedAssistant }]);
918
1123
  }
919
1124
  else {
@@ -943,8 +1148,22 @@ export const AstraApp = () => {
943
1148
  user,
944
1149
  voiceEnabled,
945
1150
  voiceListening,
1151
+ voiceWaitingForSilence,
946
1152
  workspaceRoot
947
1153
  ]);
1154
+ useEffect(() => {
1155
+ if (!voiceQueuedPrompt || !user || thinking) {
1156
+ return;
1157
+ }
1158
+ const queued = voiceQueuedPrompt.trim();
1159
+ setVoiceQueuedPrompt(null);
1160
+ if (!queued) {
1161
+ return;
1162
+ }
1163
+ pushMessage("tool", `[LOCAL] 🎙 ${queued}`);
1164
+ setPrompt("");
1165
+ void sendPrompt(queued);
1166
+ }, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
948
1167
  if (!trustedWorkspace) {
949
1168
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#c0c9db", children: "claude" }), _jsx(Text, { color: "#8ea1bd", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#f0f4ff", children: "Do you trust the files in this folder?" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#c8d5f0", children: workspaceRoot }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#8ea1bd", children: "Astra Code may read, write, or execute files contained in this directory. This can pose security risks, so only use files from trusted sources." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#7aa2ff", children: "Learn more" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: trustSelection === 0 ? "#f0f4ff" : "#8ea1bd", children: [trustSelection === 0 ? "❯ " : " ", "1. Yes, proceed"] }), _jsxs(Text, { color: trustSelection === 1 ? "#f0f4ff" : "#8ea1bd", children: [trustSelection === 1 ? "❯ " : " ", "2. No, exit"] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#8ea1bd", children: "Enter to confirm \u00B7 Esc to cancel" }) })] }));
950
1169
  }
@@ -969,17 +1188,51 @@ export const AstraApp = () => {
969
1188
  return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { color: active ? "#dce9ff" : "#7a9bba", children: [active ? "❯ " : " ", (row.title || "Untitled").slice(0, 58).padEnd(60, " ")] }), _jsxs(Text, { color: "#5a7a9a", children: [String(row.total_messages ?? 0).padStart(3, " "), " msgs \u00B7 ", formatSessionDate(row.updated_at)] })] }, row.id));
970
1189
  }) })), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { color: "#5a7a9a", children: ["Page ", historyPage + 1, " / ", historyPageCount] }), _jsxs(Text, { color: "#5a7a9a", children: ["Selected: ", selected ? selected.id : "--"] })] })] }))] }));
971
1190
  }
972
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "mode " }), _jsx(Text, { color: "#9ad5ff", children: runtimeMode })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "scope " }), _jsx(Text, { color: "#7a9bba", children: workspaceRoot })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "provider " }), _jsx(Text, { color: "#9ad5ff", children: getProviderForModel(activeModel) })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "credits " }), _jsxs(Text, { color: creditsRemaining !== null && creditsRemaining < 50 ? "#ffaa55" : "#9ad5ff", children: [creditsRemaining ?? "--", lastCreditCost !== null ? (_jsxs(Text, { color: "#5a7a9a", children: [" (-", lastCreditCost, ")"] })) : null] })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "model " }), _jsx(Text, { color: "#9ad5ff", children: activeModel })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "voice " }), _jsx(Text, { color: voiceEnabled ? "#9ad5ff" : "#5a7a9a", children: voiceEnabled ? (voiceListening ? "on/listening" : "on") : "off" })] })] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsx(Text, { color: "#3a5068", children: "/help /new /history /voice on|off|status|start|stop|input /settings /logout /exit" }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [messages.map((message, index) => {
1191
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "mode " }), _jsx(Text, { color: "#9ad5ff", children: runtimeMode })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "scope " }), _jsx(Text, { color: "#7a9bba", children: workspaceRoot })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "provider " }), _jsx(Text, { color: "#9ad5ff", children: getProviderForModel(activeModel) })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "credits " }), _jsxs(Text, { color: creditsRemaining !== null && creditsRemaining < 50 ? "#ffaa55" : "#9ad5ff", children: [creditsRemaining ?? "--", lastCreditCost !== null ? (_jsxs(Text, { color: "#5a7a9a", children: [" (-", lastCreditCost, ")"] })) : null] })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "model " }), _jsx(Text, { color: "#9ad5ff", children: activeModel })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "voice " }), _jsx(Text, { color: voiceEnabled ? "#9ad5ff" : "#5a7a9a", children: voiceEnabled ? (voiceListening ? (voiceWaitingForSilence ? "on/waiting" : "on/listening") : "on") : "off" })] })] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsx(Text, { color: "#3a5068", children: "/help /new /history /voice /voice on|off|status /settings /logout /exit" }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [messages.map((message, index) => {
973
1192
  const style = styleForKind(message.kind);
974
1193
  const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
975
1194
  const isSpacing = message.text === "" && message.kind === "system";
976
1195
  if (isSpacing) {
977
1196
  return _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: " " }) }, `${index}-spacer`);
978
1197
  }
1198
+ if (message.kind === "tool" && message.card && !isSuperAdmin) {
1199
+ const card = message.card;
1200
+ const icon = card.kind === "error"
1201
+ ? "✕"
1202
+ : card.kind === "fileDelete"
1203
+ ? "🗑"
1204
+ : card.kind === "fileCreate"
1205
+ ? "+"
1206
+ : card.kind === "fileEdit"
1207
+ ? "✎"
1208
+ : card.kind === "preview"
1209
+ ? "◉"
1210
+ : card.kind === "terminal"
1211
+ ? "⌘"
1212
+ : card.kind === "start"
1213
+ ? "…"
1214
+ : "✓";
1215
+ const accent = card.kind === "error"
1216
+ ? "#ff8d8d"
1217
+ : card.kind === "fileDelete"
1218
+ ? "#8a96a6"
1219
+ : card.kind === "fileEdit"
1220
+ ? "#b7d4ff"
1221
+ : card.kind === "fileCreate"
1222
+ ? "#a5e9c5"
1223
+ : card.kind === "preview"
1224
+ ? "#9ad5ff"
1225
+ : "#9bc5ff";
1226
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, children: paddedLabel }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: accent, children: [icon, " ", card.summary, " ", _jsxs(Text, { color: "#5a7a9a", children: ["[", card.locality, "]"] })] }), card.path ? _jsxs(Text, { color: "#6c88a8", children: ["path: ", card.path] }) : null, (card.snippetLines ?? []).slice(0, TOOL_SNIPPET_LINES).map((line, idx) => (_jsx(Text, { color: "#8ea1bd", children: line }, `${index}-snippet-${idx}`)))] })] }, `${index}-${message.kind}`));
1227
+ }
979
1228
  return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, bold: style.bold, children: paddedLabel }), message.kind === "assistant" ? (_jsx(Box, { flexDirection: "column", children: renderMarkdownContent(message.text, style.textColor, `assistant-${index}`) })) : (_jsx(Text, { color: style.textColor, bold: style.bold && message.kind === "error", children: message.text }))] }, `${index}-${message.kind}`));
980
- }), streamingText ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: styleForKind("assistant").label.padEnd(LABEL_WIDTH, " ") }), _jsx(Box, { flexDirection: "column", children: renderMarkdownContent(streamingText, "#dce9ff", "streaming") })] })) : null] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#7aa2ff", children: "◆ astra".padEnd(LABEL_WIDTH, " ") }), _jsxs(Text, { color: "#6080a0", children: [_jsx(Spinner, { type: "dots2" }), _jsx(Text, { color: "#8aa2c9", children: " thinking..." })] })] })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: "\u276F " }), _jsx(TextInput, { value: prompt, onChange: setPrompt, onSubmit: (value) => {
1229
+ }), streamingText ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: styleForKind("assistant").label.padEnd(LABEL_WIDTH, " ") }), _jsx(Box, { flexDirection: "column", children: renderMarkdownContent(streamingText, "#dce9ff", "streaming") })] })) : null] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#7aa2ff", children: "◆ astra".padEnd(LABEL_WIDTH, " ") }), _jsxs(Text, { color: "#6080a0", children: [_jsx(Spinner, { type: "dots2" }), _jsx(Text, { color: "#8aa2c9", children: " thinking..." })] })] })) : null, voiceEnabled && !thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#9ad5ff", children: "🎤 voice".padEnd(LABEL_WIDTH, " ") }), voiceListening && !voiceWaitingForSilence ? (_jsx(Text, { color: "#a6d9ff", children: "\uD83C\uDF99 listening... goblin ears activated \uD83D\uDC42" })) : voiceWaitingForSilence ? (_jsx(Text, { color: "#b7c4d8", children: "\u23F8 waiting for silence... dramatic pause loading..." })) : (_jsx(Text, { color: "#6f8199", children: "voice armed... say something when ready" }))] })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: "\u276F " }), _jsx(TextInput, { value: prompt, onSubmit: (value) => {
981
1230
  setPrompt("");
982
1231
  void sendPrompt(value);
983
- }, placeholder: voiceEnabled ? "Ask Astra... (/voice start for live transcription)" : "Ask Astra..." })] })] }));
1232
+ }, onChange: (value) => {
1233
+ if (!voiceListening) {
1234
+ setPrompt(value);
1235
+ }
1236
+ }, placeholder: voiceEnabled ? "Ask Astra... (voice on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
984
1237
  };
985
1238
  //# sourceMappingURL=App.js.map