@astra-code/astra-ai 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/app/App.js +222 -42
- package/dist/app/App.js.map +1 -1
- package/dist/lib/backendClient.js +5 -1
- package/dist/lib/backendClient.js.map +1 -1
- package/package.json +1 -1
- package/src/app/App.tsx +303 -117
- package/src/lib/backendClient.ts +5 -1
- package/src/types/events.ts +11 -0
package/src/app/App.tsx
CHANGED
|
@@ -34,6 +34,7 @@ type ToolCard = {
|
|
|
34
34
|
toolName: string;
|
|
35
35
|
locality: "LOCAL" | "REMOTE";
|
|
36
36
|
summary: string;
|
|
37
|
+
count?: number;
|
|
37
38
|
path?: string;
|
|
38
39
|
snippetLines?: string[];
|
|
39
40
|
};
|
|
@@ -58,6 +59,7 @@ const ASTRA_ASCII = `
|
|
|
58
59
|
### ### ######## ### ### ### ### ### ######## ######## ######### ##########
|
|
59
60
|
by Sean Donovan
|
|
60
61
|
`;
|
|
62
|
+
const ASTRA_COMPACT = "ASTRA CODE";
|
|
61
63
|
|
|
62
64
|
const WELCOME_WIDTH = 96;
|
|
63
65
|
|
|
@@ -74,6 +76,9 @@ const FOUNDER_WELCOME = centerLine("Welcome to Astra from Astra CEO & Founder, S
|
|
|
74
76
|
|
|
75
77
|
const HISTORY_SETTINGS_URL = "https://astra-web-builder.vercel.app/settings";
|
|
76
78
|
const VOICE_SILENCE_MS = Number(process.env.ASTRA_VOICE_SILENCE_MS ?? "3000");
|
|
79
|
+
const VOICE_MIN_CHARS = Number(process.env.ASTRA_VOICE_MIN_CHARS ?? "4");
|
|
80
|
+
const VOICE_DUPLICATE_WINDOW_MS = Number(process.env.ASTRA_VOICE_DUPLICATE_WINDOW_MS ?? "10000");
|
|
81
|
+
const VOICE_NOISE_WORDS = new Set(["you", "yes", "yeah", "yep", "ok", "okay", "uh", "um", "hmm"]);
|
|
77
82
|
const TOOL_SNIPPET_LINES = 6;
|
|
78
83
|
const NOISY_EVENT_TYPES = new Set([
|
|
79
84
|
"timing",
|
|
@@ -190,10 +195,10 @@ const extractAssistantText = (event: AgentEvent): string | null => {
|
|
|
190
195
|
return null;
|
|
191
196
|
};
|
|
192
197
|
|
|
193
|
-
const DIVIDER =
|
|
194
|
-
"────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────";
|
|
195
|
-
|
|
196
198
|
const LABEL_WIDTH = 10;
|
|
199
|
+
const HEADER_PATH_MAX = Number(process.env.ASTRA_HEADER_PATH_MAX ?? "42");
|
|
200
|
+
const MIN_DIVIDER = 64;
|
|
201
|
+
const MAX_DIVIDER = 120;
|
|
197
202
|
|
|
198
203
|
type MessageStyle = {
|
|
199
204
|
label: string;
|
|
@@ -238,14 +243,67 @@ const normalizeAssistantText = (input: string): string => {
|
|
|
238
243
|
return "";
|
|
239
244
|
}
|
|
240
245
|
|
|
241
|
-
|
|
246
|
+
let out = input
|
|
242
247
|
// Remove control chars but preserve newlines/tabs.
|
|
243
248
|
.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, "")
|
|
249
|
+
.replace(/([a-z0-9/])([A-Z])/g, "$1 $2")
|
|
250
|
+
.replace(/([.!?])([A-Za-z])/g, "$1 $2")
|
|
251
|
+
.replace(/([a-z])(\u2022)/g, "$1\n$2")
|
|
252
|
+
.replace(/([^\n])(Summary:|Next:|Tests:)/g, "$1\n\n$2")
|
|
253
|
+
.replace(/([A-Za-z0-9_/-]+)\.\s+md\b/g, "$1.md")
|
|
254
|
+
.replace(/(\bnpm audit)(All tasks complete\.)/gi, "$1\n\n$2")
|
|
255
|
+
.replace(/([.!?])\s*(Summary:|Next:|Tests:)/g, "$1\n\n$2")
|
|
256
|
+
.replace(/([^\n])(\u2022\s)/g, "$1\n$2")
|
|
244
257
|
// Trim trailing spaces line-by-line.
|
|
245
258
|
.replace(/[ \t]+$/gm, "")
|
|
246
259
|
// Normalize excessive blank lines.
|
|
247
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
248
|
-
|
|
260
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
261
|
+
|
|
262
|
+
const paragraphs = out
|
|
263
|
+
.split(/\n{2,}/)
|
|
264
|
+
.map((p) => p.trim())
|
|
265
|
+
.filter(Boolean);
|
|
266
|
+
const seen = new Set<string>();
|
|
267
|
+
const deduped: string[] = [];
|
|
268
|
+
for (const para of paragraphs) {
|
|
269
|
+
const key = para.toLowerCase().replace(/\s+/g, " ");
|
|
270
|
+
if (para.length > 50 && seen.has(key)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
seen.add(key);
|
|
274
|
+
deduped.push(para);
|
|
275
|
+
}
|
|
276
|
+
return deduped.join("\n\n").trim();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const summarizeToolResult = (toolName: string, data: Record<string, unknown>, success: boolean): string => {
|
|
280
|
+
if (!success) {
|
|
281
|
+
return `${toolName} failed`;
|
|
282
|
+
}
|
|
283
|
+
const path = typeof data.path === "string" ? data.path : "";
|
|
284
|
+
const totalLines = typeof data.total_lines === "number" ? data.total_lines : null;
|
|
285
|
+
if (toolName === "view_file") {
|
|
286
|
+
if (totalLines !== null && path) {
|
|
287
|
+
return `Read ${totalLines} lines of <${path}>`;
|
|
288
|
+
}
|
|
289
|
+
if (path) {
|
|
290
|
+
return `Read <${path}>`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (toolName === "list_directory") {
|
|
294
|
+
const dir = path || ".";
|
|
295
|
+
if (totalLines !== null) {
|
|
296
|
+
return `Listed ${totalLines} entries in <${dir}>`;
|
|
297
|
+
}
|
|
298
|
+
return `Listed <${dir}>`;
|
|
299
|
+
}
|
|
300
|
+
if (toolName === "semantic_search") {
|
|
301
|
+
return "Searched codebase context";
|
|
302
|
+
}
|
|
303
|
+
if (toolName === "search_files") {
|
|
304
|
+
return totalLines !== null ? `Found ${totalLines} matching paths` : "Searched file paths";
|
|
305
|
+
}
|
|
306
|
+
return `${toolName} completed`;
|
|
249
307
|
};
|
|
250
308
|
|
|
251
309
|
type InlineToken = {text: string; bold?: boolean; italic?: boolean; code?: boolean};
|
|
@@ -507,6 +565,20 @@ const renderMarkdownContent = (text: string, baseColor: string, keyPrefix: strin
|
|
|
507
565
|
|
|
508
566
|
export const AstraApp = (): React.JSX.Element => {
|
|
509
567
|
const workspaceRoot = useMemo(() => process.cwd(), []);
|
|
568
|
+
const [terminalWidth, setTerminalWidth] = useState<number>(() => process.stdout.columns || MAX_DIVIDER);
|
|
569
|
+
useEffect(() => {
|
|
570
|
+
const updateWidth = (): void => {
|
|
571
|
+
setTerminalWidth(process.stdout.columns || MAX_DIVIDER);
|
|
572
|
+
};
|
|
573
|
+
process.stdout.on("resize", updateWidth);
|
|
574
|
+
return () => {
|
|
575
|
+
process.stdout.off("resize", updateWidth);
|
|
576
|
+
};
|
|
577
|
+
}, []);
|
|
578
|
+
const dividerWidth = Math.max(MIN_DIVIDER, Math.min(MAX_DIVIDER, (terminalWidth ?? MAX_DIVIDER) - 2));
|
|
579
|
+
const divider = "─".repeat(dividerWidth);
|
|
580
|
+
const brand = dividerWidth < 96 ? ASTRA_COMPACT : ASTRA_ASCII;
|
|
581
|
+
const welcomeLine = dividerWidth < 96 ? "Welcome to Astra" : FOUNDER_WELCOME;
|
|
510
582
|
const backend = useMemo(() => new BackendClient(), []);
|
|
511
583
|
const {exit} = useApp();
|
|
512
584
|
|
|
@@ -565,8 +637,10 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
565
637
|
const [streamingText, setStreamingText] = useState("");
|
|
566
638
|
const [voiceEnabled, setVoiceEnabled] = useState(false);
|
|
567
639
|
const [voiceListening, setVoiceListening] = useState(false);
|
|
640
|
+
const [voicePreparing, setVoicePreparing] = useState(false);
|
|
568
641
|
const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
|
|
569
642
|
const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState<string | null>(null);
|
|
643
|
+
const [toolFeedMode, setToolFeedMode] = useState<"compact" | "expanded">("compact");
|
|
570
644
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
571
645
|
const [historyMode, setHistoryMode] = useState<HistoryMode>("picker");
|
|
572
646
|
const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
|
|
@@ -576,6 +650,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
576
650
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
577
651
|
const liveVoiceRef = useRef<LiveTranscriptionController | null>(null);
|
|
578
652
|
const voiceSilenceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
653
|
+
const lastVoicePromptRef = useRef<{text: string; at: number} | null>(null);
|
|
654
|
+
const lastIgnoredVoiceRef = useRef<{text: string; at: number} | null>(null);
|
|
579
655
|
const fileEditBuffersRef = useRef<Map<string, {path: string; chunks: string[]; toolName: string | undefined}>>(new Map());
|
|
580
656
|
const isSuperAdmin = user?.role === "super_admin";
|
|
581
657
|
|
|
@@ -584,7 +660,28 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
584
660
|
}, []);
|
|
585
661
|
|
|
586
662
|
const pushToolCard = useCallback((card: ToolCard) => {
|
|
587
|
-
setMessages((prev) =>
|
|
663
|
+
setMessages((prev) => {
|
|
664
|
+
const nextEntry = {kind: "tool", text: card.summary, card} satisfies UiMessage;
|
|
665
|
+
const last = prev[prev.length - 1];
|
|
666
|
+
if (
|
|
667
|
+
last &&
|
|
668
|
+
last.kind === "tool" &&
|
|
669
|
+
last.card &&
|
|
670
|
+
last.card.toolName === card.toolName &&
|
|
671
|
+
last.card.kind === card.kind &&
|
|
672
|
+
last.card.summary === card.summary &&
|
|
673
|
+
last.card.locality === card.locality
|
|
674
|
+
) {
|
|
675
|
+
const updated = [...prev];
|
|
676
|
+
const existingCount = Math.max(1, Number(last.card.count ?? 1));
|
|
677
|
+
updated[updated.length - 1] = {
|
|
678
|
+
...last,
|
|
679
|
+
card: {...last.card, count: existingCount + 1}
|
|
680
|
+
};
|
|
681
|
+
return updated.slice(-300);
|
|
682
|
+
}
|
|
683
|
+
return [...prev, nextEntry].slice(-300);
|
|
684
|
+
});
|
|
588
685
|
}, []);
|
|
589
686
|
|
|
590
687
|
const filteredHistory = useMemo(() => {
|
|
@@ -659,11 +756,13 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
659
756
|
}
|
|
660
757
|
const controller = liveVoiceRef.current;
|
|
661
758
|
if (!controller) {
|
|
759
|
+
setVoicePreparing(false);
|
|
662
760
|
setVoiceWaitingForSilence(false);
|
|
663
761
|
return;
|
|
664
762
|
}
|
|
665
763
|
liveVoiceRef.current = null;
|
|
666
764
|
await controller.stop();
|
|
765
|
+
setVoicePreparing(false);
|
|
667
766
|
setVoiceListening(false);
|
|
668
767
|
setVoiceWaitingForSilence(false);
|
|
669
768
|
}, []);
|
|
@@ -673,19 +772,29 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
673
772
|
return;
|
|
674
773
|
}
|
|
675
774
|
setVoiceEnabled(true);
|
|
676
|
-
|
|
775
|
+
setVoicePreparing(true);
|
|
776
|
+
setVoiceListening(false);
|
|
677
777
|
setVoiceWaitingForSilence(false);
|
|
678
778
|
if (announce) {
|
|
679
|
-
pushMessage("system", "
|
|
779
|
+
pushMessage("system", "Dictation armed. Preparing microphone...");
|
|
680
780
|
}
|
|
681
781
|
liveVoiceRef.current = startLiveTranscription({
|
|
682
782
|
onPartial: (text) => {
|
|
783
|
+
setVoicePreparing(false);
|
|
784
|
+
setVoiceListening(true);
|
|
683
785
|
setPrompt(text);
|
|
684
786
|
if (voiceSilenceTimerRef.current) {
|
|
685
787
|
clearTimeout(voiceSilenceTimerRef.current);
|
|
686
788
|
}
|
|
687
789
|
const candidate = text.trim();
|
|
688
790
|
if (!candidate) {
|
|
791
|
+
setVoiceWaitingForSilence(false);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const normalized = candidate.toLowerCase();
|
|
795
|
+
const isLikelyNoise = normalized.length < VOICE_MIN_CHARS || VOICE_NOISE_WORDS.has(normalized);
|
|
796
|
+
if (isLikelyNoise) {
|
|
797
|
+
setVoiceWaitingForSilence(false);
|
|
689
798
|
return;
|
|
690
799
|
}
|
|
691
800
|
setVoiceWaitingForSilence(true);
|
|
@@ -701,10 +810,13 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
701
810
|
}
|
|
702
811
|
setPrompt(text);
|
|
703
812
|
liveVoiceRef.current = null;
|
|
813
|
+
setVoicePreparing(false);
|
|
704
814
|
setVoiceListening(false);
|
|
705
815
|
setVoiceWaitingForSilence(false);
|
|
706
816
|
},
|
|
707
817
|
onError: (error) => {
|
|
818
|
+
setVoicePreparing(false);
|
|
819
|
+
setVoiceListening(false);
|
|
708
820
|
setVoiceWaitingForSilence(false);
|
|
709
821
|
pushMessage("error", `Voice transcription error: ${error.message}`);
|
|
710
822
|
}
|
|
@@ -934,6 +1046,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
934
1046
|
}
|
|
935
1047
|
|
|
936
1048
|
const authSession = data as AuthSession;
|
|
1049
|
+
// Set token immediately so follow-up profile/session calls are authenticated.
|
|
1050
|
+
backend.setAuthSession(authSession);
|
|
937
1051
|
const hydrated = await backend.getUserProfile(authSession).catch(() => authSession);
|
|
938
1052
|
saveSession(hydrated);
|
|
939
1053
|
backend.setAuthSession(hydrated);
|
|
@@ -1195,6 +1309,26 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1195
1309
|
|
|
1196
1310
|
if (event.type === "credits_exhausted") {
|
|
1197
1311
|
setCreditsRemaining(0);
|
|
1312
|
+
if (voiceEnabled) {
|
|
1313
|
+
await stopLiveVoice();
|
|
1314
|
+
setVoiceEnabled(false);
|
|
1315
|
+
pushMessage("system", "Dictation paused: credits exhausted. Recharge, then run /dictate on.");
|
|
1316
|
+
pushMessage("system", "");
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (event.type === "continuation_check") {
|
|
1321
|
+
const recommendation =
|
|
1322
|
+
typeof event.recommendation === "string" && event.recommendation
|
|
1323
|
+
? event.recommendation
|
|
1324
|
+
: "Please narrow the scope and continue with a specific target.";
|
|
1325
|
+
const streak = Number(event.consecutive_read_only_iterations ?? 0);
|
|
1326
|
+
const threshold = Number(event.threshold ?? 0);
|
|
1327
|
+
pushMessage(
|
|
1328
|
+
"system",
|
|
1329
|
+
`Exploration paused (${streak}/${threshold} read-only turns). ${recommendation}`
|
|
1330
|
+
);
|
|
1331
|
+
return null;
|
|
1198
1332
|
}
|
|
1199
1333
|
|
|
1200
1334
|
if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
|
|
@@ -1202,14 +1336,6 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1202
1336
|
}
|
|
1203
1337
|
|
|
1204
1338
|
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
1339
|
return null;
|
|
1214
1340
|
}
|
|
1215
1341
|
|
|
@@ -1223,11 +1349,23 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1223
1349
|
} else if (event.type === "tool_result") {
|
|
1224
1350
|
const mark = event.success ? "completed" : "failed";
|
|
1225
1351
|
const toolName = typeof event.tool_name === "string" ? event.tool_name : "tool";
|
|
1352
|
+
const payload = (event.data ?? {}) as Record<string, unknown>;
|
|
1353
|
+
const resultType = typeof event.result_type === "string" ? event.result_type : "";
|
|
1354
|
+
const alreadyRepresented =
|
|
1355
|
+
resultType === "file_create" ||
|
|
1356
|
+
resultType === "file_edit" ||
|
|
1357
|
+
resultType === "file_delete" ||
|
|
1358
|
+
toolName === "start_preview" ||
|
|
1359
|
+
toolName === "capture_screenshot";
|
|
1360
|
+
if (alreadyRepresented) {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
const summary = summarizeToolResult(toolName, payload, Boolean(event.success));
|
|
1226
1364
|
pushToolCard({
|
|
1227
1365
|
kind: event.success ? "success" : "error",
|
|
1228
1366
|
toolName,
|
|
1229
|
-
locality:
|
|
1230
|
-
summary: `${toolName} ${mark}`
|
|
1367
|
+
locality: payload.local === true ? "LOCAL" : "REMOTE",
|
|
1368
|
+
summary: event.success ? summary : `${toolName} ${mark}`
|
|
1231
1369
|
});
|
|
1232
1370
|
}
|
|
1233
1371
|
}
|
|
@@ -1239,7 +1377,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1239
1377
|
}
|
|
1240
1378
|
return null;
|
|
1241
1379
|
},
|
|
1242
|
-
[backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]
|
|
1380
|
+
[backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, stopLiveVoice, voiceEnabled, writeLocalFile]
|
|
1243
1381
|
);
|
|
1244
1382
|
|
|
1245
1383
|
const sendPrompt = useCallback(
|
|
@@ -1252,20 +1390,32 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1252
1390
|
if (text === "/help") {
|
|
1253
1391
|
pushMessage(
|
|
1254
1392
|
"system",
|
|
1255
|
-
"/new /history /
|
|
1393
|
+
"/new /history /dictate /dictate on|off|status /tools compact|expanded /settings /settings model <id> /logout /exit"
|
|
1256
1394
|
);
|
|
1257
1395
|
pushMessage("system", "");
|
|
1258
1396
|
return;
|
|
1259
1397
|
}
|
|
1398
|
+
if (text === "/tools compact") {
|
|
1399
|
+
setToolFeedMode("compact");
|
|
1400
|
+
pushMessage("system", "Tool feed set to compact.");
|
|
1401
|
+
pushMessage("system", "");
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (text === "/tools expanded") {
|
|
1405
|
+
setToolFeedMode("expanded");
|
|
1406
|
+
pushMessage("system", "Tool feed set to expanded.");
|
|
1407
|
+
pushMessage("system", "");
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1260
1410
|
if (text === "/settings") {
|
|
1261
1411
|
pushMessage(
|
|
1262
1412
|
"system",
|
|
1263
|
-
`Settings: mode=${runtimeMode} scope=${workspaceRoot} model=${activeModel} provider=${getProviderForModel(activeModel)}
|
|
1413
|
+
`Settings: mode=${runtimeMode} scope=${workspaceRoot} model=${activeModel} provider=${getProviderForModel(activeModel)} dictate=${voiceEnabled ? "on" : "off"} listening=${voiceListening ? "yes" : "no"} tool_feed=${toolFeedMode} silence_ms=${VOICE_SILENCE_MS} role=${user.role ?? "user"} client_id=${getDefaultClientId()} backend=${getBackendUrl()}`
|
|
1264
1414
|
);
|
|
1265
1415
|
pushMessage("system", "");
|
|
1266
1416
|
return;
|
|
1267
1417
|
}
|
|
1268
|
-
if (text === "/voice") {
|
|
1418
|
+
if (text === "/dictate" || text === "/voice") {
|
|
1269
1419
|
if (!voiceEnabled) {
|
|
1270
1420
|
setVoiceEnabled(true);
|
|
1271
1421
|
startLiveVoice(true);
|
|
@@ -1273,7 +1423,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1273
1423
|
}
|
|
1274
1424
|
pushMessage(
|
|
1275
1425
|
"system",
|
|
1276
|
-
`
|
|
1426
|
+
`Dictation is on${voiceListening ? " (currently listening)" : ""}. Use /dictate off to disable.`
|
|
1277
1427
|
);
|
|
1278
1428
|
pushMessage("system", "");
|
|
1279
1429
|
return;
|
|
@@ -1298,34 +1448,34 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1298
1448
|
await openHistory();
|
|
1299
1449
|
return;
|
|
1300
1450
|
}
|
|
1301
|
-
if (text === "/voice status") {
|
|
1451
|
+
if (text === "/dictate status" || text === "/voice status") {
|
|
1302
1452
|
pushMessage(
|
|
1303
1453
|
"system",
|
|
1304
|
-
`
|
|
1454
|
+
`Dictation is ${voiceEnabled ? "on" : "off"}${voicePreparing ? " (preparing mic)" : ""}${voiceListening ? " (listening)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`
|
|
1305
1455
|
);
|
|
1306
1456
|
pushMessage("system", "");
|
|
1307
1457
|
return;
|
|
1308
1458
|
}
|
|
1309
|
-
if (text === "/voice on") {
|
|
1459
|
+
if (text === "/dictate on" || text === "/voice on") {
|
|
1310
1460
|
setVoiceEnabled(true);
|
|
1311
1461
|
startLiveVoice(true);
|
|
1312
|
-
pushMessage("system", `
|
|
1462
|
+
pushMessage("system", `Dictation enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
|
|
1313
1463
|
pushMessage("system", "");
|
|
1314
1464
|
return;
|
|
1315
1465
|
}
|
|
1316
|
-
if (text === "/voice off") {
|
|
1466
|
+
if (text === "/dictate off" || text === "/voice off") {
|
|
1317
1467
|
await stopLiveVoice();
|
|
1318
1468
|
setVoiceEnabled(false);
|
|
1319
|
-
pushMessage("system", "
|
|
1469
|
+
pushMessage("system", "Dictation disabled.");
|
|
1320
1470
|
pushMessage("system", "");
|
|
1321
1471
|
return;
|
|
1322
1472
|
}
|
|
1323
|
-
if (text === "/voice input") {
|
|
1473
|
+
if (text === "/dictate input" || text === "/voice input") {
|
|
1324
1474
|
const transcribed = await transcribeOnce();
|
|
1325
1475
|
if (!transcribed) {
|
|
1326
1476
|
pushMessage(
|
|
1327
1477
|
"error",
|
|
1328
|
-
"No speech transcribed. Ensure you're signed in and
|
|
1478
|
+
"No speech transcribed. Ensure you're signed in and your microphone capture works (optional ASTRA_STT_CAPTURE_COMMAND)."
|
|
1329
1479
|
);
|
|
1330
1480
|
return;
|
|
1331
1481
|
}
|
|
@@ -1397,6 +1547,9 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1397
1547
|
localActionConfirmed = true;
|
|
1398
1548
|
}
|
|
1399
1549
|
}
|
|
1550
|
+
if (event.type === "done") {
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1400
1553
|
const piece = await handleEvent(event, activeSessionId);
|
|
1401
1554
|
if (piece) {
|
|
1402
1555
|
assistant += piece;
|
|
@@ -1436,9 +1589,11 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1436
1589
|
startLiveVoice,
|
|
1437
1590
|
stopLiveVoice,
|
|
1438
1591
|
thinking,
|
|
1592
|
+
toolFeedMode,
|
|
1439
1593
|
user,
|
|
1440
1594
|
voiceEnabled,
|
|
1441
1595
|
voiceListening,
|
|
1596
|
+
voicePreparing,
|
|
1442
1597
|
voiceWaitingForSilence,
|
|
1443
1598
|
workspaceRoot
|
|
1444
1599
|
]
|
|
@@ -1453,7 +1608,28 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1453
1608
|
if (!queued) {
|
|
1454
1609
|
return;
|
|
1455
1610
|
}
|
|
1456
|
-
|
|
1611
|
+
const normalizedQueued = queued.toLowerCase();
|
|
1612
|
+
const last = lastVoicePromptRef.current;
|
|
1613
|
+
const isLikelyNoise = normalizedQueued.length < VOICE_MIN_CHARS || VOICE_NOISE_WORDS.has(normalizedQueued);
|
|
1614
|
+
const isFastDuplicate =
|
|
1615
|
+
last !== null &&
|
|
1616
|
+
last.text === normalizedQueued &&
|
|
1617
|
+
Date.now() - last.at <= VOICE_DUPLICATE_WINDOW_MS;
|
|
1618
|
+
if (isLikelyNoise || isFastDuplicate) {
|
|
1619
|
+
const now = Date.now();
|
|
1620
|
+
const lastIgnored = lastIgnoredVoiceRef.current;
|
|
1621
|
+
const shouldLogIgnored =
|
|
1622
|
+
!lastIgnored ||
|
|
1623
|
+
lastIgnored.text !== normalizedQueued ||
|
|
1624
|
+
now - lastIgnored.at > VOICE_DUPLICATE_WINDOW_MS;
|
|
1625
|
+
if (shouldLogIgnored) {
|
|
1626
|
+
pushMessage("system", `Ignored likely-noise dictation input: "${queued}"`);
|
|
1627
|
+
lastIgnoredVoiceRef.current = {text: normalizedQueued, at: now};
|
|
1628
|
+
}
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
lastVoicePromptRef.current = {text: normalizedQueued, at: Date.now()};
|
|
1632
|
+
pushMessage("system", `Dictation input: ${queued}`);
|
|
1457
1633
|
setPrompt("");
|
|
1458
1634
|
void sendPrompt(queued);
|
|
1459
1635
|
}, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
|
|
@@ -1498,8 +1674,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1498
1674
|
if (booting) {
|
|
1499
1675
|
return (
|
|
1500
1676
|
<Box flexDirection="column">
|
|
1501
|
-
<Text color="#7aa2ff">{
|
|
1502
|
-
<Text color="#8ea1bd">{
|
|
1677
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1678
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1503
1679
|
<Text color="#8aa2c9">
|
|
1504
1680
|
<Spinner type="dots12" /> Booting Astra terminal shell...
|
|
1505
1681
|
</Text>
|
|
@@ -1510,8 +1686,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1510
1686
|
if (bootError) {
|
|
1511
1687
|
return (
|
|
1512
1688
|
<Box flexDirection="column">
|
|
1513
|
-
<Text color="#7aa2ff">{
|
|
1514
|
-
<Text color="#8ea1bd">{
|
|
1689
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1690
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1515
1691
|
<Text color="red">{bootError}</Text>
|
|
1516
1692
|
</Box>
|
|
1517
1693
|
);
|
|
@@ -1520,8 +1696,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1520
1696
|
if (!user) {
|
|
1521
1697
|
return (
|
|
1522
1698
|
<Box flexDirection="column">
|
|
1523
|
-
<Text color="#7aa2ff">{
|
|
1524
|
-
<Text color="#8ea1bd">{
|
|
1699
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1700
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1525
1701
|
<Text color="#b8c8ff">
|
|
1526
1702
|
Astra terminal AI pair programmer ({loginMode === "login" ? "Sign in" : "Create account"})
|
|
1527
1703
|
</Text>
|
|
@@ -1565,16 +1741,16 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1565
1741
|
const selected = filteredHistory[historyIndex];
|
|
1566
1742
|
return (
|
|
1567
1743
|
<Box flexDirection="column">
|
|
1568
|
-
<Text color="#7aa2ff">{
|
|
1569
|
-
<Text color="#8ea1bd">{
|
|
1570
|
-
<Text color="#2a3a50">{
|
|
1744
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1745
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1746
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1571
1747
|
{historyMode === "picker" ? (
|
|
1572
1748
|
<Box flexDirection="column">
|
|
1573
1749
|
<Box flexDirection="row" justifyContent="space-between">
|
|
1574
1750
|
<Text color="#dce9ff">History Picker</Text>
|
|
1575
1751
|
<Text color="#5a7a9a">Esc close · Enter select</Text>
|
|
1576
1752
|
</Box>
|
|
1577
|
-
<Text color="#2a3a50">{
|
|
1753
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1578
1754
|
<Box marginTop={1} flexDirection="column">
|
|
1579
1755
|
<Box flexDirection="row">
|
|
1580
1756
|
<Text color={historyPickerIndex === 0 ? "#dce9ff" : "#7a9bba"}>
|
|
@@ -1587,7 +1763,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1587
1763
|
</Text>
|
|
1588
1764
|
</Box>
|
|
1589
1765
|
</Box>
|
|
1590
|
-
<Text color="#2a3a50">{
|
|
1766
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1591
1767
|
<Text color="#5a7a9a">Credit usage history opens: {HISTORY_SETTINGS_URL}</Text>
|
|
1592
1768
|
</Box>
|
|
1593
1769
|
) : (
|
|
@@ -1600,7 +1776,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1600
1776
|
<Text color="#4a6070">search </Text>
|
|
1601
1777
|
<TextInput value={historyQuery} onChange={setHistoryQuery} placeholder="Filter chats..." />
|
|
1602
1778
|
</Box>
|
|
1603
|
-
<Text color="#2a3a50">{
|
|
1779
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1604
1780
|
{historyLoading ? (
|
|
1605
1781
|
<Text color="#8aa2c9">
|
|
1606
1782
|
<Spinner type="dots12" /> Loading chat history...
|
|
@@ -1626,7 +1802,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1626
1802
|
})}
|
|
1627
1803
|
</Box>
|
|
1628
1804
|
)}
|
|
1629
|
-
<Text color="#2a3a50">{
|
|
1805
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1630
1806
|
<Box flexDirection="row" justifyContent="space-between">
|
|
1631
1807
|
<Text color="#5a7a9a">
|
|
1632
1808
|
Page {historyPage + 1} / {historyPageCount}
|
|
@@ -1641,57 +1817,53 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1641
1817
|
|
|
1642
1818
|
return (
|
|
1643
1819
|
<Box flexDirection="column">
|
|
1644
|
-
<Text color="#7aa2ff">{
|
|
1645
|
-
<Text color="#8ea1bd">{
|
|
1820
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1821
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1646
1822
|
{/*<Text color="#9aa8c1">Astra Code · {process.cwd()}</Text>*/}
|
|
1647
|
-
<Text color="#2a3a50">{
|
|
1648
|
-
<
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
<Text color="#9ad5ff">{activeModel}</Text>
|
|
1673
|
-
</Box>
|
|
1674
|
-
<Box flexDirection="row">
|
|
1675
|
-
<Text color="#4a6070">voice </Text>
|
|
1676
|
-
<Text color={voiceEnabled ? "#9ad5ff" : "#5a7a9a"}>
|
|
1677
|
-
{voiceEnabled ? (voiceListening ? (voiceWaitingForSilence ? "on/waiting" : "on/listening") : "on") : "off"}
|
|
1678
|
-
</Text>
|
|
1679
|
-
</Box>
|
|
1680
|
-
</Box>
|
|
1681
|
-
<Text color="#2a3a50">{DIVIDER}</Text>
|
|
1682
|
-
<Text color="#3a5068">/help /new /history /voice /voice on|off|status /settings /logout /exit</Text>
|
|
1683
|
-
<Text color="#2a3a50">{DIVIDER}</Text>
|
|
1823
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1824
|
+
<Text color="#7a9bba">
|
|
1825
|
+
{`mode ${runtimeMode} · provider ${getProviderForModel(activeModel)} · model ${activeModel} · credits ${
|
|
1826
|
+
creditsRemaining ?? "--"
|
|
1827
|
+
}${lastCreditCost !== null ? ` (-${lastCreditCost})` : ""}`}
|
|
1828
|
+
</Text>
|
|
1829
|
+
<Text color="#6c88a8">
|
|
1830
|
+
{`scope ${
|
|
1831
|
+
workspaceRoot.length > HEADER_PATH_MAX ? `…${workspaceRoot.slice(-(HEADER_PATH_MAX - 1))}` : workspaceRoot
|
|
1832
|
+
} · dictate ${
|
|
1833
|
+
voiceEnabled
|
|
1834
|
+
? voicePreparing
|
|
1835
|
+
? "on/preparing"
|
|
1836
|
+
: voiceListening
|
|
1837
|
+
? voiceWaitingForSilence
|
|
1838
|
+
? "on/waiting"
|
|
1839
|
+
: "on/listening"
|
|
1840
|
+
: "on/standby"
|
|
1841
|
+
: "off"
|
|
1842
|
+
}`}
|
|
1843
|
+
</Text>
|
|
1844
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1845
|
+
<Text color="#3a5068">/help /new /history /dictate /dictate on|off|status</Text>
|
|
1846
|
+
<Text color="#3a5068">/tools compact|expanded /settings /logout /exit</Text>
|
|
1847
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1684
1848
|
<Box flexDirection="column" marginTop={1}>
|
|
1685
1849
|
{messages.map((message, index) => {
|
|
1850
|
+
const prev = index > 0 ? messages[index - 1] : null;
|
|
1686
1851
|
const style = styleForKind(message.kind);
|
|
1687
1852
|
const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
|
|
1688
1853
|
const isSpacing = message.text === "" && message.kind === "system";
|
|
1854
|
+
const needsGroupGap =
|
|
1855
|
+
Boolean(prev) &&
|
|
1856
|
+
prev?.kind !== "system" &&
|
|
1857
|
+
message.kind !== "system" &&
|
|
1858
|
+
prev?.kind !== message.kind &&
|
|
1859
|
+
(message.kind === "assistant" || message.kind === "tool");
|
|
1689
1860
|
if (isSpacing) {
|
|
1690
1861
|
return <Box key={`${index}-spacer`} marginTop={0}><Text> </Text></Box>;
|
|
1691
1862
|
}
|
|
1692
1863
|
|
|
1693
1864
|
if (message.kind === "tool" && message.card && !isSuperAdmin) {
|
|
1694
1865
|
const card = message.card;
|
|
1866
|
+
const localityLabel = card.locality === "LOCAL" ? "local" : "cloud";
|
|
1695
1867
|
const icon =
|
|
1696
1868
|
card.kind === "error"
|
|
1697
1869
|
? "✕"
|
|
@@ -1721,38 +1893,50 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1721
1893
|
? "#9ad5ff"
|
|
1722
1894
|
: "#9bc5ff";
|
|
1723
1895
|
return (
|
|
1724
|
-
<
|
|
1725
|
-
<Text
|
|
1726
|
-
<Box flexDirection="
|
|
1727
|
-
<Text color={
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
{line}
|
|
1896
|
+
<React.Fragment key={`${index}-${message.kind}`}>
|
|
1897
|
+
{needsGroupGap ? <Text> </Text> : null}
|
|
1898
|
+
<Box flexDirection="row">
|
|
1899
|
+
<Text color={style.labelColor}>{paddedLabel}</Text>
|
|
1900
|
+
<Box flexDirection="column">
|
|
1901
|
+
<Text color={accent}>
|
|
1902
|
+
{icon} {card.summary}
|
|
1903
|
+
{card.count && card.count > 1 ? <Text color="#9cb8d8"> (x{card.count})</Text> : null}
|
|
1904
|
+
<Text color="#5a7a9a"> · {localityLabel}</Text>
|
|
1734
1905
|
</Text>
|
|
1735
|
-
|
|
1906
|
+
{toolFeedMode === "expanded" ? (
|
|
1907
|
+
<>
|
|
1908
|
+
{card.path ? <Text color="#6c88a8">path: {card.path}</Text> : null}
|
|
1909
|
+
{(card.snippetLines ?? []).slice(0, TOOL_SNIPPET_LINES).map((line, idx) => (
|
|
1910
|
+
<Text key={`${index}-snippet-${idx}`} color="#8ea1bd">
|
|
1911
|
+
{line}
|
|
1912
|
+
</Text>
|
|
1913
|
+
))}
|
|
1914
|
+
</>
|
|
1915
|
+
) : null}
|
|
1916
|
+
</Box>
|
|
1736
1917
|
</Box>
|
|
1737
|
-
</
|
|
1918
|
+
</React.Fragment>
|
|
1738
1919
|
);
|
|
1739
1920
|
}
|
|
1740
1921
|
|
|
1741
1922
|
return (
|
|
1742
|
-
<
|
|
1743
|
-
<Text
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
<Box flexDirection="column">
|
|
1748
|
-
{renderMarkdownContent(message.text, style.textColor, `assistant-${index}`)}
|
|
1749
|
-
</Box>
|
|
1750
|
-
) : (
|
|
1751
|
-
<Text color={style.textColor} bold={style.bold && message.kind === "error"}>
|
|
1752
|
-
{message.text}
|
|
1923
|
+
<React.Fragment key={`${index}-${message.kind}`}>
|
|
1924
|
+
{needsGroupGap ? <Text> </Text> : null}
|
|
1925
|
+
<Box flexDirection="row">
|
|
1926
|
+
<Text color={style.labelColor} bold={style.bold}>
|
|
1927
|
+
{paddedLabel}
|
|
1753
1928
|
</Text>
|
|
1754
|
-
|
|
1755
|
-
|
|
1929
|
+
{message.kind === "assistant" ? (
|
|
1930
|
+
<Box flexDirection="column">
|
|
1931
|
+
{renderMarkdownContent(message.text, style.textColor, `assistant-${index}`)}
|
|
1932
|
+
</Box>
|
|
1933
|
+
) : (
|
|
1934
|
+
<Text color={style.textColor} bold={style.bold && message.kind === "error"}>
|
|
1935
|
+
{message.text}
|
|
1936
|
+
</Text>
|
|
1937
|
+
)}
|
|
1938
|
+
</Box>
|
|
1939
|
+
</React.Fragment>
|
|
1756
1940
|
);
|
|
1757
1941
|
})}
|
|
1758
1942
|
{streamingText ? (
|
|
@@ -1762,7 +1946,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1762
1946
|
</Box>
|
|
1763
1947
|
) : null}
|
|
1764
1948
|
</Box>
|
|
1765
|
-
<Text color="#2a3a50">{
|
|
1949
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1766
1950
|
{thinking ? (
|
|
1767
1951
|
<Box flexDirection="row" marginTop={1}>
|
|
1768
1952
|
<Text color="#7aa2ff">{"◆ astra".padEnd(LABEL_WIDTH, " ")}</Text>
|
|
@@ -1774,13 +1958,15 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1774
1958
|
) : null}
|
|
1775
1959
|
{voiceEnabled && !thinking ? (
|
|
1776
1960
|
<Box flexDirection="row" marginTop={1}>
|
|
1777
|
-
<Text color="#9ad5ff">{"🎤
|
|
1778
|
-
{
|
|
1779
|
-
<Text color="#
|
|
1961
|
+
<Text color="#9ad5ff">{"🎤 dictate".padEnd(LABEL_WIDTH, " ")}</Text>
|
|
1962
|
+
{voicePreparing ? (
|
|
1963
|
+
<Text color="#f4d58a">🟡 preparing microphone...</Text>
|
|
1964
|
+
) : voiceListening && !voiceWaitingForSilence ? (
|
|
1965
|
+
<Text color="#9de3b4">🟢 listening now - speak clearly</Text>
|
|
1780
1966
|
) : voiceWaitingForSilence ? (
|
|
1781
|
-
<Text color="#b7c4d8"
|
|
1967
|
+
<Text color="#b7c4d8">⏳ speech detected - waiting for silence to send</Text>
|
|
1782
1968
|
) : (
|
|
1783
|
-
<Text color="#6f8199"
|
|
1969
|
+
<Text color="#6f8199">⚪ voice armed - preparing next listen window</Text>
|
|
1784
1970
|
)}
|
|
1785
1971
|
</Box>
|
|
1786
1972
|
) : null}
|
|
@@ -1797,7 +1983,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1797
1983
|
setPrompt(value);
|
|
1798
1984
|
}
|
|
1799
1985
|
}}
|
|
1800
|
-
placeholder={voiceEnabled ? "Ask Astra... (
|
|
1986
|
+
placeholder={voiceEnabled ? "Ask Astra... (dictate on: auto listen + send on silence)" : "Ask Astra..."}
|
|
1801
1987
|
/>
|
|
1802
1988
|
</Box>
|
|
1803
1989
|
</Box>
|