@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/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") {
@@ -53,8 +65,11 @@ const eventToToolLine = (event) => {
53
65
  const mark = success ? "✓" : "✗";
54
66
  const toolName = event.tool_name ?? "tool";
55
67
  const payload = (event.data ?? {});
68
+ const screenshotPath = typeof payload.path === "string" ? payload.path : "";
56
69
  const output = String(payload.output ??
57
70
  payload.content ??
71
+ payload.summary ??
72
+ (toolName === "capture_screenshot" && screenshotPath ? `Saved screenshot: ${screenshotPath}` : undefined) ??
58
73
  event.error ??
59
74
  "");
60
75
  const locality = payload.local === true ? "LOCAL" : "REMOTE";
@@ -70,6 +85,26 @@ const eventToToolLine = (event) => {
70
85
  }
71
86
  return null;
72
87
  };
88
+ const extractSnippetLines = (content, maxLines = TOOL_SNIPPET_LINES) => {
89
+ if (!content) {
90
+ return [];
91
+ }
92
+ const lines = content.split("\n").map((line) => line.trimEnd());
93
+ if (lines.length <= maxLines) {
94
+ return lines;
95
+ }
96
+ return lines.slice(0, maxLines);
97
+ };
98
+ const extractDiffSnippet = (diff, maxLines = TOOL_SNIPPET_LINES) => {
99
+ if (!Array.isArray(diff)) {
100
+ return [];
101
+ }
102
+ const interesting = diff
103
+ .map((line) => String(line))
104
+ .filter((line) => line.startsWith("+") || line.startsWith("-"))
105
+ .map((line) => line.slice(0, 220));
106
+ return interesting.slice(0, maxLines);
107
+ };
73
108
  const looksLikeLocalFilesystemClaim = (text) => {
74
109
  const lower = text.toLowerCase();
75
110
  const changeWord = lower.includes("created") ||
@@ -365,6 +400,8 @@ export const AstraApp = () => {
365
400
  const [streamingText, setStreamingText] = useState("");
366
401
  const [voiceEnabled, setVoiceEnabled] = useState(false);
367
402
  const [voiceListening, setVoiceListening] = useState(false);
403
+ const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
404
+ const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState(null);
368
405
  const [historyOpen, setHistoryOpen] = useState(false);
369
406
  const [historyMode, setHistoryMode] = useState("picker");
370
407
  const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
@@ -373,9 +410,15 @@ export const AstraApp = () => {
373
410
  const [historyRows, setHistoryRows] = useState([]);
374
411
  const [historyIndex, setHistoryIndex] = useState(0);
375
412
  const liveVoiceRef = useRef(null);
413
+ const voiceSilenceTimerRef = useRef(null);
414
+ const fileEditBuffersRef = useRef(new Map());
415
+ const isSuperAdmin = user?.role === "super_admin";
376
416
  const pushMessage = useCallback((kind, text) => {
377
417
  setMessages((prev) => [...prev, { kind, text }].slice(-300));
378
418
  }, []);
419
+ const pushToolCard = useCallback((card) => {
420
+ setMessages((prev) => [...prev, { kind: "tool", text: card.summary, card }].slice(-300));
421
+ }, []);
379
422
  const filteredHistory = useMemo(() => {
380
423
  const q = historyQuery.trim().toLowerCase();
381
424
  if (!q) {
@@ -439,41 +482,80 @@ export const AstraApp = () => {
439
482
  }
440
483
  }, [backend, pushMessage, user]);
441
484
  const stopLiveVoice = useCallback(async () => {
485
+ if (voiceSilenceTimerRef.current) {
486
+ clearTimeout(voiceSilenceTimerRef.current);
487
+ voiceSilenceTimerRef.current = null;
488
+ }
442
489
  const controller = liveVoiceRef.current;
443
490
  if (!controller) {
491
+ setVoiceWaitingForSilence(false);
444
492
  return;
445
493
  }
446
494
  liveVoiceRef.current = null;
447
495
  await controller.stop();
448
496
  setVoiceListening(false);
497
+ setVoiceWaitingForSilence(false);
449
498
  }, []);
450
- const startLiveVoice = useCallback(() => {
499
+ const startLiveVoice = useCallback((announce = true) => {
451
500
  if (liveVoiceRef.current) {
452
501
  return;
453
502
  }
454
503
  setVoiceEnabled(true);
455
504
  setVoiceListening(true);
456
- pushMessage("system", "Live transcription started. Speak now…");
505
+ setVoiceWaitingForSilence(false);
506
+ if (announce) {
507
+ pushMessage("system", "Voice input started. Speak now…");
508
+ }
457
509
  liveVoiceRef.current = startLiveTranscription({
458
510
  onPartial: (text) => {
459
511
  setPrompt(text);
512
+ if (voiceSilenceTimerRef.current) {
513
+ clearTimeout(voiceSilenceTimerRef.current);
514
+ }
515
+ const candidate = text.trim();
516
+ if (!candidate) {
517
+ return;
518
+ }
519
+ setVoiceWaitingForSilence(true);
520
+ voiceSilenceTimerRef.current = setTimeout(() => {
521
+ setVoiceQueuedPrompt(candidate);
522
+ void stopLiveVoice();
523
+ }, VOICE_SILENCE_MS);
460
524
  },
461
525
  onFinal: (text) => {
526
+ if (voiceSilenceTimerRef.current) {
527
+ clearTimeout(voiceSilenceTimerRef.current);
528
+ voiceSilenceTimerRef.current = null;
529
+ }
462
530
  setPrompt(text);
531
+ liveVoiceRef.current = null;
532
+ setVoiceListening(false);
533
+ setVoiceWaitingForSilence(false);
463
534
  },
464
535
  onError: (error) => {
536
+ setVoiceWaitingForSilence(false);
465
537
  pushMessage("error", `Voice transcription error: ${error.message}`);
466
538
  }
467
539
  });
468
- }, [pushMessage]);
540
+ }, [pushMessage, stopLiveVoice]);
469
541
  useEffect(() => {
470
542
  return () => {
543
+ if (voiceSilenceTimerRef.current) {
544
+ clearTimeout(voiceSilenceTimerRef.current);
545
+ voiceSilenceTimerRef.current = null;
546
+ }
471
547
  const controller = liveVoiceRef.current;
472
548
  if (controller) {
473
549
  void controller.stop();
474
550
  }
475
551
  };
476
552
  }, []);
553
+ useEffect(() => {
554
+ if (!voiceEnabled || !user || thinking || voiceListening || liveVoiceRef.current) {
555
+ return;
556
+ }
557
+ startLiveVoice(false);
558
+ }, [startLiveVoice, thinking, user, voiceEnabled, voiceListening]);
477
559
  useEffect(() => {
478
560
  if (!trustedWorkspace) {
479
561
  return;
@@ -490,6 +572,7 @@ export const AstraApp = () => {
490
572
  }
491
573
  const cached = loadSession();
492
574
  if (!cached) {
575
+ backend.setAuthSession(null);
493
576
  if (!cancelled) {
494
577
  setBooting(false);
495
578
  }
@@ -498,9 +581,13 @@ export const AstraApp = () => {
498
581
  const valid = await backend.validateSession(cached);
499
582
  if (!valid) {
500
583
  clearSession();
584
+ backend.setAuthSession(null);
501
585
  }
502
586
  else if (!cancelled) {
503
- setUser(cached);
587
+ const hydrated = await backend.getUserProfile(cached).catch(() => cached);
588
+ saveSession(hydrated);
589
+ backend.setAuthSession(hydrated);
590
+ setUser(hydrated);
504
591
  setMessages([
505
592
  { kind: "system", text: "Welcome back. Type a message or /help." },
506
593
  { kind: "system", text: "" }
@@ -629,7 +716,7 @@ export const AstraApp = () => {
629
716
  try {
630
717
  await backend.deleteSession(selected.id);
631
718
  setHistoryRows((prev) => prev.filter((row) => row.id !== selected.id));
632
- pushMessage("tool", `Archived session: ${selected.title}`);
719
+ pushMessage("system", `Archived session: ${selected.title}`);
633
720
  }
634
721
  catch (error) {
635
722
  pushMessage("error", `Failed to delete session: ${error instanceof Error ? error.message : String(error)}`);
@@ -662,8 +749,10 @@ export const AstraApp = () => {
662
749
  throw new Error(data.error);
663
750
  }
664
751
  const authSession = data;
665
- saveSession(authSession);
666
- setUser(authSession);
752
+ const hydrated = await backend.getUserProfile(authSession).catch(() => authSession);
753
+ saveSession(hydrated);
754
+ backend.setAuthSession(hydrated);
755
+ setUser(hydrated);
667
756
  setEmail("");
668
757
  setPassword("");
669
758
  setMessages([
@@ -679,8 +768,80 @@ export const AstraApp = () => {
679
768
  }
680
769
  }, [backend, email, loginMode, password]);
681
770
  const handleEvent = useCallback(async (event, activeSessionId) => {
771
+ if (event.type === "file_edit_start") {
772
+ const data = (event.data ?? {});
773
+ const toolId = typeof event.tool_id === "string"
774
+ ? event.tool_id
775
+ : typeof data.tool_id === "string"
776
+ ? data.tool_id
777
+ : "";
778
+ const toolName = typeof event.tool_name === "string"
779
+ ? event.tool_name
780
+ : typeof data.tool_name === "string"
781
+ ? data.tool_name
782
+ : undefined;
783
+ if (toolId) {
784
+ const current = fileEditBuffersRef.current.get(toolId) ?? { path: "(unknown)", chunks: [], toolName: undefined };
785
+ fileEditBuffersRef.current.set(toolId, { ...current, toolName });
786
+ }
787
+ return null;
788
+ }
789
+ if (event.type === "file_edit_path") {
790
+ const data = (event.data ?? {});
791
+ const toolId = typeof event.tool_id === "string"
792
+ ? event.tool_id
793
+ : typeof data.tool_id === "string"
794
+ ? data.tool_id
795
+ : "";
796
+ const path = typeof event.path === "string"
797
+ ? event.path
798
+ : typeof data.path === "string"
799
+ ? data.path
800
+ : "(unknown)";
801
+ const toolName = typeof event.tool_name === "string"
802
+ ? event.tool_name
803
+ : typeof data.tool_name === "string"
804
+ ? data.tool_name
805
+ : undefined;
806
+ if (toolId) {
807
+ const current = fileEditBuffersRef.current.get(toolId);
808
+ fileEditBuffersRef.current.set(toolId, { path, chunks: [], toolName: toolName ?? current?.toolName });
809
+ }
810
+ if (!isSuperAdmin) {
811
+ const resolvedToolName = toolName ?? fileEditBuffersRef.current.get(toolId)?.toolName;
812
+ const usingBulkWriter = resolvedToolName === "bulk_file_writer";
813
+ pushToolCard({
814
+ kind: "fileEdit",
815
+ toolName: resolvedToolName ?? "edit_file",
816
+ locality: "LOCAL",
817
+ summary: usingBulkWriter ? `Writing file ${path}` : `Editing ${path}`,
818
+ path
819
+ });
820
+ }
821
+ return null;
822
+ }
823
+ if (event.type === "file_edit_delta") {
824
+ const data = (event.data ?? {});
825
+ const toolId = typeof event.tool_id === "string"
826
+ ? event.tool_id
827
+ : typeof data.tool_id === "string"
828
+ ? data.tool_id
829
+ : "";
830
+ const chunk = typeof event.content === "string" ? event.content : "";
831
+ if (toolId && chunk) {
832
+ const current = fileEditBuffersRef.current.get(toolId) ?? { path: "(unknown)", chunks: [], toolName: undefined };
833
+ current.chunks.push(chunk);
834
+ fileEditBuffersRef.current.set(toolId, current);
835
+ }
836
+ if (isSuperAdmin) {
837
+ const raw = JSON.stringify(event);
838
+ pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
839
+ }
840
+ return null;
841
+ }
682
842
  const assistantPiece = extractAssistantText(event);
683
- if (assistantPiece && !["tool_result", "error", "credits_update", "credits_exhausted"].includes(event.type)) {
843
+ if (assistantPiece &&
844
+ !["tool_result", "error", "credits_update", "credits_exhausted", "file_edit_delta", "file_edit_path"].includes(event.type)) {
684
845
  return assistantPiece;
685
846
  }
686
847
  if (event.type === "run_in_terminal") {
@@ -698,20 +859,45 @@ export const AstraApp = () => {
698
859
  cancelled: result.cancelled
699
860
  });
700
861
  }
701
- pushMessage("tool", `[LOCAL] ▸ ran: ${command.slice(0, 60)}...`);
862
+ if (isSuperAdmin) {
863
+ pushMessage("tool", `[LOCAL] ▸ ran: ${command.slice(0, 60)}...`);
864
+ }
865
+ else {
866
+ pushToolCard({
867
+ kind: "terminal",
868
+ toolName: "run_in_terminal",
869
+ locality: "LOCAL",
870
+ summary: `Running command: ${command.slice(0, 80)}`
871
+ });
872
+ }
702
873
  return null;
703
874
  }
704
875
  // Apply file operations to the local workspace immediately.
705
876
  if (event.type === "tool_result" && event.success) {
706
877
  const d = (event.data ?? {});
707
878
  const resultType = event.result_type;
879
+ const toolResultId = typeof event.tool_call_id === "string" ? event.tool_call_id : "";
708
880
  const relPath = typeof d.path === "string" ? d.path : null;
709
881
  const lang = typeof d.language === "string" ? d.language : "plaintext";
882
+ const locality = d.local === true ? "LOCAL" : "REMOTE";
710
883
  if (relPath) {
711
884
  if (resultType === "file_create") {
712
885
  const content = typeof d.content === "string" ? d.content : "";
713
886
  writeLocalFile(relPath, content, lang);
714
- pushMessage("tool", `[LOCAL] wrote ${relPath}`);
887
+ const snippetLines = extractSnippetLines(content);
888
+ if (isSuperAdmin) {
889
+ pushMessage("tool", `[LOCAL] ✓ wrote ${relPath}`);
890
+ }
891
+ else {
892
+ pushToolCard({
893
+ kind: "fileCreate",
894
+ toolName: "create_file",
895
+ locality,
896
+ summary: `Created ${relPath}`,
897
+ path: relPath,
898
+ snippetLines
899
+ });
900
+ }
715
901
  }
716
902
  else if (resultType === "file_edit") {
717
903
  const content = typeof d.full_new_content === "string"
@@ -721,14 +907,78 @@ export const AstraApp = () => {
721
907
  : null;
722
908
  if (content !== null) {
723
909
  writeLocalFile(relPath, content, lang);
724
- pushMessage("tool", `[LOCAL] edited ${relPath}`);
910
+ const snippetLines = extractDiffSnippet(d.diff) ?? extractSnippetLines(content);
911
+ if (isSuperAdmin) {
912
+ pushMessage("tool", `[LOCAL] ✓ edited ${relPath}`);
913
+ }
914
+ else {
915
+ const toolId = toolResultId;
916
+ const streamed = toolId ? fileEditBuffersRef.current.get(toolId) : undefined;
917
+ const streamedSnippet = streamed ? extractSnippetLines(streamed.chunks.join("")) : [];
918
+ pushToolCard({
919
+ kind: "fileEdit",
920
+ toolName: "edit_file",
921
+ locality,
922
+ summary: `Updated ${relPath}`,
923
+ path: relPath,
924
+ snippetLines: streamedSnippet.length > 0 ? streamedSnippet : snippetLines
925
+ });
926
+ if (toolId) {
927
+ fileEditBuffersRef.current.delete(toolId);
928
+ }
929
+ }
725
930
  }
726
931
  }
727
932
  else if (resultType === "file_delete") {
728
933
  deleteLocalFile(relPath);
729
- pushMessage("tool", `[LOCAL] ✓ deleted ${relPath}`);
934
+ if (isSuperAdmin) {
935
+ pushMessage("tool", `[LOCAL] ✓ deleted ${relPath}`);
936
+ }
937
+ else {
938
+ pushToolCard({
939
+ kind: "fileDelete",
940
+ toolName: "delete_file",
941
+ locality,
942
+ summary: `Deleted ${relPath}`,
943
+ path: relPath
944
+ });
945
+ }
730
946
  }
731
947
  }
948
+ if (!isSuperAdmin && event.tool_name === "start_preview") {
949
+ const message = typeof d.message === "string"
950
+ ? d.message
951
+ : typeof d.preview_url === "string"
952
+ ? `Preview: ${d.preview_url}`
953
+ : "Local preview started.";
954
+ pushToolCard({
955
+ kind: "preview",
956
+ toolName: "start_preview",
957
+ locality,
958
+ summary: message
959
+ });
960
+ return null;
961
+ }
962
+ if (!isSuperAdmin && event.tool_name === "capture_screenshot") {
963
+ const screenshotPath = typeof d.path === "string" ? d.path : "";
964
+ const targetUrl = typeof d.url === "string" ? d.url : "";
965
+ const message = screenshotPath
966
+ ? `Screenshot saved: ${screenshotPath}`
967
+ : targetUrl
968
+ ? `Captured screenshot: ${targetUrl}`
969
+ : "Screenshot captured.";
970
+ pushToolCard({
971
+ kind: "preview",
972
+ toolName: "capture_screenshot",
973
+ locality,
974
+ summary: message,
975
+ ...(screenshotPath ? { path: screenshotPath } : {})
976
+ });
977
+ return null;
978
+ }
979
+ if (toolResultId) {
980
+ fileEditBuffersRef.current.delete(toolResultId);
981
+ }
732
982
  }
733
983
  if (event.type === "credits_update") {
734
984
  const remaining = Number(event.remaining ?? 0);
@@ -744,33 +994,71 @@ export const AstraApp = () => {
744
994
  if (event.type === "credits_exhausted") {
745
995
  setCreditsRemaining(0);
746
996
  }
997
+ if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
998
+ return null;
999
+ }
1000
+ if (!isSuperAdmin && event.type === "tool_start") {
1001
+ const tool = event.tool;
1002
+ const name = tool?.name ?? "tool";
1003
+ pushToolCard({
1004
+ kind: "start",
1005
+ toolName: name,
1006
+ locality: "REMOTE",
1007
+ summary: `${name} is running...`
1008
+ });
1009
+ return null;
1010
+ }
747
1011
  const toolLine = eventToToolLine(event);
748
1012
  if (toolLine) {
749
1013
  if (event.type === "error" || event.type === "credits_exhausted") {
750
1014
  pushMessage("error", toolLine);
751
1015
  }
752
1016
  else {
753
- pushMessage("tool", toolLine);
1017
+ if (isSuperAdmin) {
1018
+ pushMessage("tool", toolLine);
1019
+ }
1020
+ else if (event.type === "tool_result") {
1021
+ const mark = event.success ? "completed" : "failed";
1022
+ const toolName = typeof event.tool_name === "string" ? event.tool_name : "tool";
1023
+ pushToolCard({
1024
+ kind: event.success ? "success" : "error",
1025
+ toolName,
1026
+ locality: (event.data ?? {}).local === true ? "LOCAL" : "REMOTE",
1027
+ summary: `${toolName} ${mark}`
1028
+ });
1029
+ }
754
1030
  }
755
1031
  }
756
1032
  else if (event.type !== "thinking") {
757
- const raw = JSON.stringify(event);
758
- pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
1033
+ if (isSuperAdmin) {
1034
+ const raw = JSON.stringify(event);
1035
+ pushMessage("tool", `event ${event.type}: ${raw.slice(0, 220)}`);
1036
+ }
759
1037
  }
760
1038
  return null;
761
- }, [backend, deleteLocalFile, pushMessage, writeLocalFile]);
1039
+ }, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]);
762
1040
  const sendPrompt = useCallback(async (rawPrompt) => {
763
1041
  const text = rawPrompt.trim();
764
1042
  if (!text || !user || thinking) {
765
1043
  return;
766
1044
  }
767
1045
  if (text === "/help") {
768
- pushMessage("system", "/new /history /voice on|off|status|start|stop|input /settings /settings model <id> /logout /exit");
1046
+ pushMessage("system", "/new /history /voice /voice on|off|status /settings /settings model <id> /logout /exit");
769
1047
  pushMessage("system", "");
770
1048
  return;
771
1049
  }
772
1050
  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()}`);
1051
+ 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()}`);
1052
+ pushMessage("system", "");
1053
+ return;
1054
+ }
1055
+ if (text === "/voice") {
1056
+ if (!voiceEnabled) {
1057
+ setVoiceEnabled(true);
1058
+ startLiveVoice(true);
1059
+ return;
1060
+ }
1061
+ pushMessage("system", `Voice input is on${voiceListening ? " (currently listening)" : ""}. Use /voice off to disable.`);
774
1062
  pushMessage("system", "");
775
1063
  return;
776
1064
  }
@@ -792,50 +1080,31 @@ export const AstraApp = () => {
792
1080
  return;
793
1081
  }
794
1082
  if (text === "/voice status") {
795
- pushMessage("system", `Voice mode is ${voiceEnabled ? "on" : "off"}${voiceListening ? " (live transcription active)" : ""}.`);
1083
+ pushMessage("system", `Voice input is ${voiceEnabled ? "on" : "off"}${voiceListening ? " (live transcription active)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`);
796
1084
  pushMessage("system", "");
797
1085
  return;
798
1086
  }
799
1087
  if (text === "/voice on") {
800
1088
  setVoiceEnabled(true);
801
- pushMessage("system", "Voice mode enabled (assistant responses will be spoken).");
1089
+ startLiveVoice(true);
1090
+ pushMessage("system", `Voice input enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
802
1091
  pushMessage("system", "");
803
1092
  return;
804
1093
  }
805
1094
  if (text === "/voice off") {
806
1095
  await stopLiveVoice();
807
1096
  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.");
1097
+ pushMessage("system", "Voice input disabled.");
827
1098
  pushMessage("system", "");
828
1099
  return;
829
1100
  }
830
1101
  if (text === "/voice input") {
831
1102
  const transcribed = await transcribeOnce();
832
1103
  if (!transcribed) {
833
- pushMessage("error", "No speech transcribed. Set ASTRA_STT_COMMAND to a command that prints transcript text to stdout.");
1104
+ pushMessage("error", "No speech transcribed. Ensure you're signed in and that mic capture works (optional ASTRA_STT_CAPTURE_COMMAND).");
834
1105
  return;
835
1106
  }
836
- pushMessage("tool", `[LOCAL] 🎙 ${transcribed}`);
837
- setPrompt(transcribed);
838
- pushMessage("system", "Transcribed input ready. Press Enter to send.");
1107
+ setVoiceQueuedPrompt(transcribed.trim());
839
1108
  return;
840
1109
  }
841
1110
  if (text === "/new") {
@@ -849,6 +1118,7 @@ export const AstraApp = () => {
849
1118
  if (text === "/logout") {
850
1119
  await stopLiveVoice();
851
1120
  clearSession();
1121
+ backend.setAuthSession(null);
852
1122
  setUser(null);
853
1123
  setMessages([]);
854
1124
  setChatMessages([]);
@@ -911,9 +1181,6 @@ export const AstraApp = () => {
911
1181
  ? `Remote result (not yet confirmed as local filesystem change): ${cleanedAssistant}`
912
1182
  : cleanedAssistant;
913
1183
  pushMessage("assistant", guardedAssistant);
914
- if (voiceEnabled) {
915
- speakText(guardedAssistant);
916
- }
917
1184
  setChatMessages((prev) => [...prev, { role: "assistant", content: cleanedAssistant }]);
918
1185
  }
919
1186
  else {
@@ -943,8 +1210,22 @@ export const AstraApp = () => {
943
1210
  user,
944
1211
  voiceEnabled,
945
1212
  voiceListening,
1213
+ voiceWaitingForSilence,
946
1214
  workspaceRoot
947
1215
  ]);
1216
+ useEffect(() => {
1217
+ if (!voiceQueuedPrompt || !user || thinking) {
1218
+ return;
1219
+ }
1220
+ const queued = voiceQueuedPrompt.trim();
1221
+ setVoiceQueuedPrompt(null);
1222
+ if (!queued) {
1223
+ return;
1224
+ }
1225
+ pushMessage("system", `Voice input: ${queued}`);
1226
+ setPrompt("");
1227
+ void sendPrompt(queued);
1228
+ }, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
948
1229
  if (!trustedWorkspace) {
949
1230
  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
1231
  }
@@ -969,17 +1250,51 @@ export const AstraApp = () => {
969
1250
  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
1251
  }) })), _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
1252
  }
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) => {
1253
+ 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
1254
  const style = styleForKind(message.kind);
974
1255
  const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
975
1256
  const isSpacing = message.text === "" && message.kind === "system";
976
1257
  if (isSpacing) {
977
1258
  return _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: " " }) }, `${index}-spacer`);
978
1259
  }
1260
+ if (message.kind === "tool" && message.card && !isSuperAdmin) {
1261
+ const card = message.card;
1262
+ const icon = card.kind === "error"
1263
+ ? "✕"
1264
+ : card.kind === "fileDelete"
1265
+ ? "🗑"
1266
+ : card.kind === "fileCreate"
1267
+ ? "+"
1268
+ : card.kind === "fileEdit"
1269
+ ? "✎"
1270
+ : card.kind === "preview"
1271
+ ? "◉"
1272
+ : card.kind === "terminal"
1273
+ ? "⌘"
1274
+ : card.kind === "start"
1275
+ ? "…"
1276
+ : "✓";
1277
+ const accent = card.kind === "error"
1278
+ ? "#ff8d8d"
1279
+ : card.kind === "fileDelete"
1280
+ ? "#8a96a6"
1281
+ : card.kind === "fileEdit"
1282
+ ? "#b7d4ff"
1283
+ : card.kind === "fileCreate"
1284
+ ? "#a5e9c5"
1285
+ : card.kind === "preview"
1286
+ ? "#9ad5ff"
1287
+ : "#9bc5ff";
1288
+ 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}`));
1289
+ }
979
1290
  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) => {
1291
+ }), 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
1292
  setPrompt("");
982
1293
  void sendPrompt(value);
983
- }, placeholder: voiceEnabled ? "Ask Astra... (/voice start for live transcription)" : "Ask Astra..." })] })] }));
1294
+ }, onChange: (value) => {
1295
+ if (!voiceListening) {
1296
+ setPrompt(value);
1297
+ }
1298
+ }, placeholder: voiceEnabled ? "Ask Astra... (voice on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
984
1299
  };
985
1300
  //# sourceMappingURL=App.js.map