@astra-code/astra-ai 0.1.3 → 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 +220 -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 +301 -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
|
}
|
|
@@ -1197,6 +1309,26 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1197
1309
|
|
|
1198
1310
|
if (event.type === "credits_exhausted") {
|
|
1199
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;
|
|
1200
1332
|
}
|
|
1201
1333
|
|
|
1202
1334
|
if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
|
|
@@ -1204,14 +1336,6 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1204
1336
|
}
|
|
1205
1337
|
|
|
1206
1338
|
if (!isSuperAdmin && event.type === "tool_start") {
|
|
1207
|
-
const tool = event.tool as {name?: string} | undefined;
|
|
1208
|
-
const name = tool?.name ?? "tool";
|
|
1209
|
-
pushToolCard({
|
|
1210
|
-
kind: "start",
|
|
1211
|
-
toolName: name,
|
|
1212
|
-
locality: "REMOTE",
|
|
1213
|
-
summary: `${name} is running...`
|
|
1214
|
-
});
|
|
1215
1339
|
return null;
|
|
1216
1340
|
}
|
|
1217
1341
|
|
|
@@ -1225,11 +1349,23 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1225
1349
|
} else if (event.type === "tool_result") {
|
|
1226
1350
|
const mark = event.success ? "completed" : "failed";
|
|
1227
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));
|
|
1228
1364
|
pushToolCard({
|
|
1229
1365
|
kind: event.success ? "success" : "error",
|
|
1230
1366
|
toolName,
|
|
1231
|
-
locality:
|
|
1232
|
-
summary: `${toolName} ${mark}`
|
|
1367
|
+
locality: payload.local === true ? "LOCAL" : "REMOTE",
|
|
1368
|
+
summary: event.success ? summary : `${toolName} ${mark}`
|
|
1233
1369
|
});
|
|
1234
1370
|
}
|
|
1235
1371
|
}
|
|
@@ -1241,7 +1377,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1241
1377
|
}
|
|
1242
1378
|
return null;
|
|
1243
1379
|
},
|
|
1244
|
-
[backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]
|
|
1380
|
+
[backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, stopLiveVoice, voiceEnabled, writeLocalFile]
|
|
1245
1381
|
);
|
|
1246
1382
|
|
|
1247
1383
|
const sendPrompt = useCallback(
|
|
@@ -1254,20 +1390,32 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1254
1390
|
if (text === "/help") {
|
|
1255
1391
|
pushMessage(
|
|
1256
1392
|
"system",
|
|
1257
|
-
"/new /history /
|
|
1393
|
+
"/new /history /dictate /dictate on|off|status /tools compact|expanded /settings /settings model <id> /logout /exit"
|
|
1258
1394
|
);
|
|
1259
1395
|
pushMessage("system", "");
|
|
1260
1396
|
return;
|
|
1261
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
|
+
}
|
|
1262
1410
|
if (text === "/settings") {
|
|
1263
1411
|
pushMessage(
|
|
1264
1412
|
"system",
|
|
1265
|
-
`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()}`
|
|
1266
1414
|
);
|
|
1267
1415
|
pushMessage("system", "");
|
|
1268
1416
|
return;
|
|
1269
1417
|
}
|
|
1270
|
-
if (text === "/voice") {
|
|
1418
|
+
if (text === "/dictate" || text === "/voice") {
|
|
1271
1419
|
if (!voiceEnabled) {
|
|
1272
1420
|
setVoiceEnabled(true);
|
|
1273
1421
|
startLiveVoice(true);
|
|
@@ -1275,7 +1423,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1275
1423
|
}
|
|
1276
1424
|
pushMessage(
|
|
1277
1425
|
"system",
|
|
1278
|
-
`
|
|
1426
|
+
`Dictation is on${voiceListening ? " (currently listening)" : ""}. Use /dictate off to disable.`
|
|
1279
1427
|
);
|
|
1280
1428
|
pushMessage("system", "");
|
|
1281
1429
|
return;
|
|
@@ -1300,34 +1448,34 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1300
1448
|
await openHistory();
|
|
1301
1449
|
return;
|
|
1302
1450
|
}
|
|
1303
|
-
if (text === "/voice status") {
|
|
1451
|
+
if (text === "/dictate status" || text === "/voice status") {
|
|
1304
1452
|
pushMessage(
|
|
1305
1453
|
"system",
|
|
1306
|
-
`
|
|
1454
|
+
`Dictation is ${voiceEnabled ? "on" : "off"}${voicePreparing ? " (preparing mic)" : ""}${voiceListening ? " (listening)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`
|
|
1307
1455
|
);
|
|
1308
1456
|
pushMessage("system", "");
|
|
1309
1457
|
return;
|
|
1310
1458
|
}
|
|
1311
|
-
if (text === "/voice on") {
|
|
1459
|
+
if (text === "/dictate on" || text === "/voice on") {
|
|
1312
1460
|
setVoiceEnabled(true);
|
|
1313
1461
|
startLiveVoice(true);
|
|
1314
|
-
pushMessage("system", `
|
|
1462
|
+
pushMessage("system", `Dictation enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
|
|
1315
1463
|
pushMessage("system", "");
|
|
1316
1464
|
return;
|
|
1317
1465
|
}
|
|
1318
|
-
if (text === "/voice off") {
|
|
1466
|
+
if (text === "/dictate off" || text === "/voice off") {
|
|
1319
1467
|
await stopLiveVoice();
|
|
1320
1468
|
setVoiceEnabled(false);
|
|
1321
|
-
pushMessage("system", "
|
|
1469
|
+
pushMessage("system", "Dictation disabled.");
|
|
1322
1470
|
pushMessage("system", "");
|
|
1323
1471
|
return;
|
|
1324
1472
|
}
|
|
1325
|
-
if (text === "/voice input") {
|
|
1473
|
+
if (text === "/dictate input" || text === "/voice input") {
|
|
1326
1474
|
const transcribed = await transcribeOnce();
|
|
1327
1475
|
if (!transcribed) {
|
|
1328
1476
|
pushMessage(
|
|
1329
1477
|
"error",
|
|
1330
|
-
"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)."
|
|
1331
1479
|
);
|
|
1332
1480
|
return;
|
|
1333
1481
|
}
|
|
@@ -1399,6 +1547,9 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1399
1547
|
localActionConfirmed = true;
|
|
1400
1548
|
}
|
|
1401
1549
|
}
|
|
1550
|
+
if (event.type === "done") {
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1402
1553
|
const piece = await handleEvent(event, activeSessionId);
|
|
1403
1554
|
if (piece) {
|
|
1404
1555
|
assistant += piece;
|
|
@@ -1438,9 +1589,11 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1438
1589
|
startLiveVoice,
|
|
1439
1590
|
stopLiveVoice,
|
|
1440
1591
|
thinking,
|
|
1592
|
+
toolFeedMode,
|
|
1441
1593
|
user,
|
|
1442
1594
|
voiceEnabled,
|
|
1443
1595
|
voiceListening,
|
|
1596
|
+
voicePreparing,
|
|
1444
1597
|
voiceWaitingForSilence,
|
|
1445
1598
|
workspaceRoot
|
|
1446
1599
|
]
|
|
@@ -1455,7 +1608,28 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1455
1608
|
if (!queued) {
|
|
1456
1609
|
return;
|
|
1457
1610
|
}
|
|
1458
|
-
|
|
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}`);
|
|
1459
1633
|
setPrompt("");
|
|
1460
1634
|
void sendPrompt(queued);
|
|
1461
1635
|
}, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
|
|
@@ -1500,8 +1674,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1500
1674
|
if (booting) {
|
|
1501
1675
|
return (
|
|
1502
1676
|
<Box flexDirection="column">
|
|
1503
|
-
<Text color="#7aa2ff">{
|
|
1504
|
-
<Text color="#8ea1bd">{
|
|
1677
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1678
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1505
1679
|
<Text color="#8aa2c9">
|
|
1506
1680
|
<Spinner type="dots12" /> Booting Astra terminal shell...
|
|
1507
1681
|
</Text>
|
|
@@ -1512,8 +1686,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1512
1686
|
if (bootError) {
|
|
1513
1687
|
return (
|
|
1514
1688
|
<Box flexDirection="column">
|
|
1515
|
-
<Text color="#7aa2ff">{
|
|
1516
|
-
<Text color="#8ea1bd">{
|
|
1689
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1690
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1517
1691
|
<Text color="red">{bootError}</Text>
|
|
1518
1692
|
</Box>
|
|
1519
1693
|
);
|
|
@@ -1522,8 +1696,8 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1522
1696
|
if (!user) {
|
|
1523
1697
|
return (
|
|
1524
1698
|
<Box flexDirection="column">
|
|
1525
|
-
<Text color="#7aa2ff">{
|
|
1526
|
-
<Text color="#8ea1bd">{
|
|
1699
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1700
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1527
1701
|
<Text color="#b8c8ff">
|
|
1528
1702
|
Astra terminal AI pair programmer ({loginMode === "login" ? "Sign in" : "Create account"})
|
|
1529
1703
|
</Text>
|
|
@@ -1567,16 +1741,16 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1567
1741
|
const selected = filteredHistory[historyIndex];
|
|
1568
1742
|
return (
|
|
1569
1743
|
<Box flexDirection="column">
|
|
1570
|
-
<Text color="#7aa2ff">{
|
|
1571
|
-
<Text color="#8ea1bd">{
|
|
1572
|
-
<Text color="#2a3a50">{
|
|
1744
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1745
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1746
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1573
1747
|
{historyMode === "picker" ? (
|
|
1574
1748
|
<Box flexDirection="column">
|
|
1575
1749
|
<Box flexDirection="row" justifyContent="space-between">
|
|
1576
1750
|
<Text color="#dce9ff">History Picker</Text>
|
|
1577
1751
|
<Text color="#5a7a9a">Esc close · Enter select</Text>
|
|
1578
1752
|
</Box>
|
|
1579
|
-
<Text color="#2a3a50">{
|
|
1753
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1580
1754
|
<Box marginTop={1} flexDirection="column">
|
|
1581
1755
|
<Box flexDirection="row">
|
|
1582
1756
|
<Text color={historyPickerIndex === 0 ? "#dce9ff" : "#7a9bba"}>
|
|
@@ -1589,7 +1763,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1589
1763
|
</Text>
|
|
1590
1764
|
</Box>
|
|
1591
1765
|
</Box>
|
|
1592
|
-
<Text color="#2a3a50">{
|
|
1766
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1593
1767
|
<Text color="#5a7a9a">Credit usage history opens: {HISTORY_SETTINGS_URL}</Text>
|
|
1594
1768
|
</Box>
|
|
1595
1769
|
) : (
|
|
@@ -1602,7 +1776,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1602
1776
|
<Text color="#4a6070">search </Text>
|
|
1603
1777
|
<TextInput value={historyQuery} onChange={setHistoryQuery} placeholder="Filter chats..." />
|
|
1604
1778
|
</Box>
|
|
1605
|
-
<Text color="#2a3a50">{
|
|
1779
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1606
1780
|
{historyLoading ? (
|
|
1607
1781
|
<Text color="#8aa2c9">
|
|
1608
1782
|
<Spinner type="dots12" /> Loading chat history...
|
|
@@ -1628,7 +1802,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1628
1802
|
})}
|
|
1629
1803
|
</Box>
|
|
1630
1804
|
)}
|
|
1631
|
-
<Text color="#2a3a50">{
|
|
1805
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1632
1806
|
<Box flexDirection="row" justifyContent="space-between">
|
|
1633
1807
|
<Text color="#5a7a9a">
|
|
1634
1808
|
Page {historyPage + 1} / {historyPageCount}
|
|
@@ -1643,57 +1817,53 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1643
1817
|
|
|
1644
1818
|
return (
|
|
1645
1819
|
<Box flexDirection="column">
|
|
1646
|
-
<Text color="#7aa2ff">{
|
|
1647
|
-
<Text color="#8ea1bd">{
|
|
1820
|
+
<Text color="#7aa2ff">{brand}</Text>
|
|
1821
|
+
<Text color="#8ea1bd">{welcomeLine}</Text>
|
|
1648
1822
|
{/*<Text color="#9aa8c1">Astra Code · {process.cwd()}</Text>*/}
|
|
1649
|
-
<Text color="#2a3a50">{
|
|
1650
|
-
<
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
<Text color="#9ad5ff">{activeModel}</Text>
|
|
1675
|
-
</Box>
|
|
1676
|
-
<Box flexDirection="row">
|
|
1677
|
-
<Text color="#4a6070">voice </Text>
|
|
1678
|
-
<Text color={voiceEnabled ? "#9ad5ff" : "#5a7a9a"}>
|
|
1679
|
-
{voiceEnabled ? (voiceListening ? (voiceWaitingForSilence ? "on/waiting" : "on/listening") : "on") : "off"}
|
|
1680
|
-
</Text>
|
|
1681
|
-
</Box>
|
|
1682
|
-
</Box>
|
|
1683
|
-
<Text color="#2a3a50">{DIVIDER}</Text>
|
|
1684
|
-
<Text color="#3a5068">/help /new /history /voice /voice on|off|status /settings /logout /exit</Text>
|
|
1685
|
-
<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>
|
|
1686
1848
|
<Box flexDirection="column" marginTop={1}>
|
|
1687
1849
|
{messages.map((message, index) => {
|
|
1850
|
+
const prev = index > 0 ? messages[index - 1] : null;
|
|
1688
1851
|
const style = styleForKind(message.kind);
|
|
1689
1852
|
const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
|
|
1690
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");
|
|
1691
1860
|
if (isSpacing) {
|
|
1692
1861
|
return <Box key={`${index}-spacer`} marginTop={0}><Text> </Text></Box>;
|
|
1693
1862
|
}
|
|
1694
1863
|
|
|
1695
1864
|
if (message.kind === "tool" && message.card && !isSuperAdmin) {
|
|
1696
1865
|
const card = message.card;
|
|
1866
|
+
const localityLabel = card.locality === "LOCAL" ? "local" : "cloud";
|
|
1697
1867
|
const icon =
|
|
1698
1868
|
card.kind === "error"
|
|
1699
1869
|
? "✕"
|
|
@@ -1723,38 +1893,50 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1723
1893
|
? "#9ad5ff"
|
|
1724
1894
|
: "#9bc5ff";
|
|
1725
1895
|
return (
|
|
1726
|
-
<
|
|
1727
|
-
<Text
|
|
1728
|
-
<Box flexDirection="
|
|
1729
|
-
<Text color={
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
{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>
|
|
1736
1905
|
</Text>
|
|
1737
|
-
|
|
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>
|
|
1738
1917
|
</Box>
|
|
1739
|
-
</
|
|
1918
|
+
</React.Fragment>
|
|
1740
1919
|
);
|
|
1741
1920
|
}
|
|
1742
1921
|
|
|
1743
1922
|
return (
|
|
1744
|
-
<
|
|
1745
|
-
<Text
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
<Box flexDirection="column">
|
|
1750
|
-
{renderMarkdownContent(message.text, style.textColor, `assistant-${index}`)}
|
|
1751
|
-
</Box>
|
|
1752
|
-
) : (
|
|
1753
|
-
<Text color={style.textColor} bold={style.bold && message.kind === "error"}>
|
|
1754
|
-
{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}
|
|
1755
1928
|
</Text>
|
|
1756
|
-
|
|
1757
|
-
|
|
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>
|
|
1758
1940
|
);
|
|
1759
1941
|
})}
|
|
1760
1942
|
{streamingText ? (
|
|
@@ -1764,7 +1946,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1764
1946
|
</Box>
|
|
1765
1947
|
) : null}
|
|
1766
1948
|
</Box>
|
|
1767
|
-
<Text color="#2a3a50">{
|
|
1949
|
+
<Text color="#2a3a50">{divider}</Text>
|
|
1768
1950
|
{thinking ? (
|
|
1769
1951
|
<Box flexDirection="row" marginTop={1}>
|
|
1770
1952
|
<Text color="#7aa2ff">{"◆ astra".padEnd(LABEL_WIDTH, " ")}</Text>
|
|
@@ -1776,13 +1958,15 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1776
1958
|
) : null}
|
|
1777
1959
|
{voiceEnabled && !thinking ? (
|
|
1778
1960
|
<Box flexDirection="row" marginTop={1}>
|
|
1779
|
-
<Text color="#9ad5ff">{"🎤
|
|
1780
|
-
{
|
|
1781
|
-
<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>
|
|
1782
1966
|
) : voiceWaitingForSilence ? (
|
|
1783
|
-
<Text color="#b7c4d8"
|
|
1967
|
+
<Text color="#b7c4d8">⏳ speech detected - waiting for silence to send</Text>
|
|
1784
1968
|
) : (
|
|
1785
|
-
<Text color="#6f8199"
|
|
1969
|
+
<Text color="#6f8199">⚪ voice armed - preparing next listen window</Text>
|
|
1786
1970
|
)}
|
|
1787
1971
|
</Box>
|
|
1788
1972
|
) : null}
|
|
@@ -1799,7 +1983,7 @@ export const AstraApp = (): React.JSX.Element => {
|
|
|
1799
1983
|
setPrompt(value);
|
|
1800
1984
|
}
|
|
1801
1985
|
}}
|
|
1802
|
-
placeholder={voiceEnabled ? "Ask Astra... (
|
|
1986
|
+
placeholder={voiceEnabled ? "Ask Astra... (dictate on: auto listen + send on silence)" : "Ask Astra..."}
|
|
1803
1987
|
/>
|
|
1804
1988
|
</Box>
|
|
1805
1989
|
</Box>
|