@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/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
`astra-code-term` is the npm-first Astra terminal client built with TypeScript, React, and Ink.
|
|
4
4
|
|
|
5
|
+
## Repository structure
|
|
6
|
+
|
|
7
|
+
- `.env.example` — sample configuration
|
|
8
|
+
- `docs/` — supplemental documentation
|
|
9
|
+
- `install.sh` — helper script for setup
|
|
10
|
+
- `src/` — Ink CLI source code (entrypoint in `src/index.ts`)
|
|
11
|
+
- `tsconfig.json` — shared TypeScript configuration
|
|
12
|
+
|
|
5
13
|
## Install
|
|
6
14
|
|
|
7
15
|
### Global (recommended)
|
package/dist/app/App.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, useApp, useInput } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
@@ -32,6 +32,7 @@ const ASTRA_ASCII = `
|
|
|
32
32
|
### ### ######## ### ### ### ### ### ######## ######## ######### ##########
|
|
33
33
|
by Sean Donovan
|
|
34
34
|
`;
|
|
35
|
+
const ASTRA_COMPACT = "ASTRA CODE";
|
|
35
36
|
const WELCOME_WIDTH = 96;
|
|
36
37
|
const centerLine = (text, width = WELCOME_WIDTH) => {
|
|
37
38
|
const trimmed = text.trim();
|
|
@@ -44,6 +45,9 @@ const centerLine = (text, width = WELCOME_WIDTH) => {
|
|
|
44
45
|
const FOUNDER_WELCOME = centerLine("Welcome to Astra from Astra CEO & Founder, Sean Donovan");
|
|
45
46
|
const HISTORY_SETTINGS_URL = "https://astra-web-builder.vercel.app/settings";
|
|
46
47
|
const VOICE_SILENCE_MS = Number(process.env.ASTRA_VOICE_SILENCE_MS ?? "3000");
|
|
48
|
+
const VOICE_MIN_CHARS = Number(process.env.ASTRA_VOICE_MIN_CHARS ?? "4");
|
|
49
|
+
const VOICE_DUPLICATE_WINDOW_MS = Number(process.env.ASTRA_VOICE_DUPLICATE_WINDOW_MS ?? "10000");
|
|
50
|
+
const VOICE_NOISE_WORDS = new Set(["you", "yes", "yeah", "yep", "ok", "okay", "uh", "um", "hmm"]);
|
|
47
51
|
const TOOL_SNIPPET_LINES = 6;
|
|
48
52
|
const NOISY_EVENT_TYPES = new Set([
|
|
49
53
|
"timing",
|
|
@@ -145,8 +149,10 @@ const extractAssistantText = (event) => {
|
|
|
145
149
|
}
|
|
146
150
|
return null;
|
|
147
151
|
};
|
|
148
|
-
const DIVIDER = "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────";
|
|
149
152
|
const LABEL_WIDTH = 10;
|
|
153
|
+
const HEADER_PATH_MAX = Number(process.env.ASTRA_HEADER_PATH_MAX ?? "42");
|
|
154
|
+
const MIN_DIVIDER = 64;
|
|
155
|
+
const MAX_DIVIDER = 120;
|
|
150
156
|
const styleForKind = (kind) => {
|
|
151
157
|
switch (kind) {
|
|
152
158
|
case "assistant":
|
|
@@ -183,14 +189,65 @@ const normalizeAssistantText = (input) => {
|
|
|
183
189
|
if (!input) {
|
|
184
190
|
return "";
|
|
185
191
|
}
|
|
186
|
-
|
|
192
|
+
let out = input
|
|
187
193
|
// Remove control chars but preserve newlines/tabs.
|
|
188
194
|
.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, "")
|
|
195
|
+
.replace(/([a-z0-9/])([A-Z])/g, "$1 $2")
|
|
196
|
+
.replace(/([.!?])([A-Za-z])/g, "$1 $2")
|
|
197
|
+
.replace(/([a-z])(\u2022)/g, "$1\n$2")
|
|
198
|
+
.replace(/([^\n])(Summary:|Next:|Tests:)/g, "$1\n\n$2")
|
|
199
|
+
.replace(/([A-Za-z0-9_/-]+)\.\s+md\b/g, "$1.md")
|
|
200
|
+
.replace(/(\bnpm audit)(All tasks complete\.)/gi, "$1\n\n$2")
|
|
201
|
+
.replace(/([.!?])\s*(Summary:|Next:|Tests:)/g, "$1\n\n$2")
|
|
202
|
+
.replace(/([^\n])(\u2022\s)/g, "$1\n$2")
|
|
189
203
|
// Trim trailing spaces line-by-line.
|
|
190
204
|
.replace(/[ \t]+$/gm, "")
|
|
191
205
|
// Normalize excessive blank lines.
|
|
192
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
193
|
-
|
|
206
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
207
|
+
const paragraphs = out
|
|
208
|
+
.split(/\n{2,}/)
|
|
209
|
+
.map((p) => p.trim())
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
const seen = new Set();
|
|
212
|
+
const deduped = [];
|
|
213
|
+
for (const para of paragraphs) {
|
|
214
|
+
const key = para.toLowerCase().replace(/\s+/g, " ");
|
|
215
|
+
if (para.length > 50 && seen.has(key)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
seen.add(key);
|
|
219
|
+
deduped.push(para);
|
|
220
|
+
}
|
|
221
|
+
return deduped.join("\n\n").trim();
|
|
222
|
+
};
|
|
223
|
+
const summarizeToolResult = (toolName, data, success) => {
|
|
224
|
+
if (!success) {
|
|
225
|
+
return `${toolName} failed`;
|
|
226
|
+
}
|
|
227
|
+
const path = typeof data.path === "string" ? data.path : "";
|
|
228
|
+
const totalLines = typeof data.total_lines === "number" ? data.total_lines : null;
|
|
229
|
+
if (toolName === "view_file") {
|
|
230
|
+
if (totalLines !== null && path) {
|
|
231
|
+
return `Read ${totalLines} lines of <${path}>`;
|
|
232
|
+
}
|
|
233
|
+
if (path) {
|
|
234
|
+
return `Read <${path}>`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (toolName === "list_directory") {
|
|
238
|
+
const dir = path || ".";
|
|
239
|
+
if (totalLines !== null) {
|
|
240
|
+
return `Listed ${totalLines} entries in <${dir}>`;
|
|
241
|
+
}
|
|
242
|
+
return `Listed <${dir}>`;
|
|
243
|
+
}
|
|
244
|
+
if (toolName === "semantic_search") {
|
|
245
|
+
return "Searched codebase context";
|
|
246
|
+
}
|
|
247
|
+
if (toolName === "search_files") {
|
|
248
|
+
return totalLines !== null ? `Found ${totalLines} matching paths` : "Searched file paths";
|
|
249
|
+
}
|
|
250
|
+
return `${toolName} completed`;
|
|
194
251
|
};
|
|
195
252
|
const parseInline = (line) => {
|
|
196
253
|
const tokens = [];
|
|
@@ -351,6 +408,20 @@ const renderMarkdownContent = (text, baseColor, keyPrefix) => {
|
|
|
351
408
|
};
|
|
352
409
|
export const AstraApp = () => {
|
|
353
410
|
const workspaceRoot = useMemo(() => process.cwd(), []);
|
|
411
|
+
const [terminalWidth, setTerminalWidth] = useState(() => process.stdout.columns || MAX_DIVIDER);
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
const updateWidth = () => {
|
|
414
|
+
setTerminalWidth(process.stdout.columns || MAX_DIVIDER);
|
|
415
|
+
};
|
|
416
|
+
process.stdout.on("resize", updateWidth);
|
|
417
|
+
return () => {
|
|
418
|
+
process.stdout.off("resize", updateWidth);
|
|
419
|
+
};
|
|
420
|
+
}, []);
|
|
421
|
+
const dividerWidth = Math.max(MIN_DIVIDER, Math.min(MAX_DIVIDER, (terminalWidth ?? MAX_DIVIDER) - 2));
|
|
422
|
+
const divider = "─".repeat(dividerWidth);
|
|
423
|
+
const brand = dividerWidth < 96 ? ASTRA_COMPACT : ASTRA_ASCII;
|
|
424
|
+
const welcomeLine = dividerWidth < 96 ? "Welcome to Astra" : FOUNDER_WELCOME;
|
|
354
425
|
const backend = useMemo(() => new BackendClient(), []);
|
|
355
426
|
const { exit } = useApp();
|
|
356
427
|
// In-session file cache: tracks files created/edited so subsequent requests
|
|
@@ -400,8 +471,10 @@ export const AstraApp = () => {
|
|
|
400
471
|
const [streamingText, setStreamingText] = useState("");
|
|
401
472
|
const [voiceEnabled, setVoiceEnabled] = useState(false);
|
|
402
473
|
const [voiceListening, setVoiceListening] = useState(false);
|
|
474
|
+
const [voicePreparing, setVoicePreparing] = useState(false);
|
|
403
475
|
const [voiceWaitingForSilence, setVoiceWaitingForSilence] = useState(false);
|
|
404
476
|
const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState(null);
|
|
477
|
+
const [toolFeedMode, setToolFeedMode] = useState("compact");
|
|
405
478
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
406
479
|
const [historyMode, setHistoryMode] = useState("picker");
|
|
407
480
|
const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
|
|
@@ -411,13 +484,34 @@ export const AstraApp = () => {
|
|
|
411
484
|
const [historyIndex, setHistoryIndex] = useState(0);
|
|
412
485
|
const liveVoiceRef = useRef(null);
|
|
413
486
|
const voiceSilenceTimerRef = useRef(null);
|
|
487
|
+
const lastVoicePromptRef = useRef(null);
|
|
488
|
+
const lastIgnoredVoiceRef = useRef(null);
|
|
414
489
|
const fileEditBuffersRef = useRef(new Map());
|
|
415
490
|
const isSuperAdmin = user?.role === "super_admin";
|
|
416
491
|
const pushMessage = useCallback((kind, text) => {
|
|
417
492
|
setMessages((prev) => [...prev, { kind, text }].slice(-300));
|
|
418
493
|
}, []);
|
|
419
494
|
const pushToolCard = useCallback((card) => {
|
|
420
|
-
setMessages((prev) =>
|
|
495
|
+
setMessages((prev) => {
|
|
496
|
+
const nextEntry = { kind: "tool", text: card.summary, card };
|
|
497
|
+
const last = prev[prev.length - 1];
|
|
498
|
+
if (last &&
|
|
499
|
+
last.kind === "tool" &&
|
|
500
|
+
last.card &&
|
|
501
|
+
last.card.toolName === card.toolName &&
|
|
502
|
+
last.card.kind === card.kind &&
|
|
503
|
+
last.card.summary === card.summary &&
|
|
504
|
+
last.card.locality === card.locality) {
|
|
505
|
+
const updated = [...prev];
|
|
506
|
+
const existingCount = Math.max(1, Number(last.card.count ?? 1));
|
|
507
|
+
updated[updated.length - 1] = {
|
|
508
|
+
...last,
|
|
509
|
+
card: { ...last.card, count: existingCount + 1 }
|
|
510
|
+
};
|
|
511
|
+
return updated.slice(-300);
|
|
512
|
+
}
|
|
513
|
+
return [...prev, nextEntry].slice(-300);
|
|
514
|
+
});
|
|
421
515
|
}, []);
|
|
422
516
|
const filteredHistory = useMemo(() => {
|
|
423
517
|
const q = historyQuery.trim().toLowerCase();
|
|
@@ -488,11 +582,13 @@ export const AstraApp = () => {
|
|
|
488
582
|
}
|
|
489
583
|
const controller = liveVoiceRef.current;
|
|
490
584
|
if (!controller) {
|
|
585
|
+
setVoicePreparing(false);
|
|
491
586
|
setVoiceWaitingForSilence(false);
|
|
492
587
|
return;
|
|
493
588
|
}
|
|
494
589
|
liveVoiceRef.current = null;
|
|
495
590
|
await controller.stop();
|
|
591
|
+
setVoicePreparing(false);
|
|
496
592
|
setVoiceListening(false);
|
|
497
593
|
setVoiceWaitingForSilence(false);
|
|
498
594
|
}, []);
|
|
@@ -501,19 +597,29 @@ export const AstraApp = () => {
|
|
|
501
597
|
return;
|
|
502
598
|
}
|
|
503
599
|
setVoiceEnabled(true);
|
|
504
|
-
|
|
600
|
+
setVoicePreparing(true);
|
|
601
|
+
setVoiceListening(false);
|
|
505
602
|
setVoiceWaitingForSilence(false);
|
|
506
603
|
if (announce) {
|
|
507
|
-
pushMessage("system", "
|
|
604
|
+
pushMessage("system", "Dictation armed. Preparing microphone...");
|
|
508
605
|
}
|
|
509
606
|
liveVoiceRef.current = startLiveTranscription({
|
|
510
607
|
onPartial: (text) => {
|
|
608
|
+
setVoicePreparing(false);
|
|
609
|
+
setVoiceListening(true);
|
|
511
610
|
setPrompt(text);
|
|
512
611
|
if (voiceSilenceTimerRef.current) {
|
|
513
612
|
clearTimeout(voiceSilenceTimerRef.current);
|
|
514
613
|
}
|
|
515
614
|
const candidate = text.trim();
|
|
516
615
|
if (!candidate) {
|
|
616
|
+
setVoiceWaitingForSilence(false);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const normalized = candidate.toLowerCase();
|
|
620
|
+
const isLikelyNoise = normalized.length < VOICE_MIN_CHARS || VOICE_NOISE_WORDS.has(normalized);
|
|
621
|
+
if (isLikelyNoise) {
|
|
622
|
+
setVoiceWaitingForSilence(false);
|
|
517
623
|
return;
|
|
518
624
|
}
|
|
519
625
|
setVoiceWaitingForSilence(true);
|
|
@@ -529,10 +635,13 @@ export const AstraApp = () => {
|
|
|
529
635
|
}
|
|
530
636
|
setPrompt(text);
|
|
531
637
|
liveVoiceRef.current = null;
|
|
638
|
+
setVoicePreparing(false);
|
|
532
639
|
setVoiceListening(false);
|
|
533
640
|
setVoiceWaitingForSilence(false);
|
|
534
641
|
},
|
|
535
642
|
onError: (error) => {
|
|
643
|
+
setVoicePreparing(false);
|
|
644
|
+
setVoiceListening(false);
|
|
536
645
|
setVoiceWaitingForSilence(false);
|
|
537
646
|
pushMessage("error", `Voice transcription error: ${error.message}`);
|
|
538
647
|
}
|
|
@@ -995,19 +1104,26 @@ export const AstraApp = () => {
|
|
|
995
1104
|
}
|
|
996
1105
|
if (event.type === "credits_exhausted") {
|
|
997
1106
|
setCreditsRemaining(0);
|
|
1107
|
+
if (voiceEnabled) {
|
|
1108
|
+
await stopLiveVoice();
|
|
1109
|
+
setVoiceEnabled(false);
|
|
1110
|
+
pushMessage("system", "Dictation paused: credits exhausted. Recharge, then run /dictate on.");
|
|
1111
|
+
pushMessage("system", "");
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (event.type === "continuation_check") {
|
|
1115
|
+
const recommendation = typeof event.recommendation === "string" && event.recommendation
|
|
1116
|
+
? event.recommendation
|
|
1117
|
+
: "Please narrow the scope and continue with a specific target.";
|
|
1118
|
+
const streak = Number(event.consecutive_read_only_iterations ?? 0);
|
|
1119
|
+
const threshold = Number(event.threshold ?? 0);
|
|
1120
|
+
pushMessage("system", `Exploration paused (${streak}/${threshold} read-only turns). ${recommendation}`);
|
|
1121
|
+
return null;
|
|
998
1122
|
}
|
|
999
1123
|
if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
|
|
1000
1124
|
return null;
|
|
1001
1125
|
}
|
|
1002
1126
|
if (!isSuperAdmin && event.type === "tool_start") {
|
|
1003
|
-
const tool = event.tool;
|
|
1004
|
-
const name = tool?.name ?? "tool";
|
|
1005
|
-
pushToolCard({
|
|
1006
|
-
kind: "start",
|
|
1007
|
-
toolName: name,
|
|
1008
|
-
locality: "REMOTE",
|
|
1009
|
-
summary: `${name} is running...`
|
|
1010
|
-
});
|
|
1011
1127
|
return null;
|
|
1012
1128
|
}
|
|
1013
1129
|
const toolLine = eventToToolLine(event);
|
|
@@ -1022,11 +1138,22 @@ export const AstraApp = () => {
|
|
|
1022
1138
|
else if (event.type === "tool_result") {
|
|
1023
1139
|
const mark = event.success ? "completed" : "failed";
|
|
1024
1140
|
const toolName = typeof event.tool_name === "string" ? event.tool_name : "tool";
|
|
1141
|
+
const payload = (event.data ?? {});
|
|
1142
|
+
const resultType = typeof event.result_type === "string" ? event.result_type : "";
|
|
1143
|
+
const alreadyRepresented = resultType === "file_create" ||
|
|
1144
|
+
resultType === "file_edit" ||
|
|
1145
|
+
resultType === "file_delete" ||
|
|
1146
|
+
toolName === "start_preview" ||
|
|
1147
|
+
toolName === "capture_screenshot";
|
|
1148
|
+
if (alreadyRepresented) {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
const summary = summarizeToolResult(toolName, payload, Boolean(event.success));
|
|
1025
1152
|
pushToolCard({
|
|
1026
1153
|
kind: event.success ? "success" : "error",
|
|
1027
1154
|
toolName,
|
|
1028
|
-
locality:
|
|
1029
|
-
summary: `${toolName} ${mark}`
|
|
1155
|
+
locality: payload.local === true ? "LOCAL" : "REMOTE",
|
|
1156
|
+
summary: event.success ? summary : `${toolName} ${mark}`
|
|
1030
1157
|
});
|
|
1031
1158
|
}
|
|
1032
1159
|
}
|
|
@@ -1038,29 +1165,41 @@ export const AstraApp = () => {
|
|
|
1038
1165
|
}
|
|
1039
1166
|
}
|
|
1040
1167
|
return null;
|
|
1041
|
-
}, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]);
|
|
1168
|
+
}, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, stopLiveVoice, voiceEnabled, writeLocalFile]);
|
|
1042
1169
|
const sendPrompt = useCallback(async (rawPrompt) => {
|
|
1043
1170
|
const text = rawPrompt.trim();
|
|
1044
1171
|
if (!text || !user || thinking) {
|
|
1045
1172
|
return;
|
|
1046
1173
|
}
|
|
1047
1174
|
if (text === "/help") {
|
|
1048
|
-
pushMessage("system", "/new /history /
|
|
1175
|
+
pushMessage("system", "/new /history /dictate /dictate on|off|status /tools compact|expanded /settings /settings model <id> /logout /exit");
|
|
1176
|
+
pushMessage("system", "");
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (text === "/tools compact") {
|
|
1180
|
+
setToolFeedMode("compact");
|
|
1181
|
+
pushMessage("system", "Tool feed set to compact.");
|
|
1182
|
+
pushMessage("system", "");
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (text === "/tools expanded") {
|
|
1186
|
+
setToolFeedMode("expanded");
|
|
1187
|
+
pushMessage("system", "Tool feed set to expanded.");
|
|
1049
1188
|
pushMessage("system", "");
|
|
1050
1189
|
return;
|
|
1051
1190
|
}
|
|
1052
1191
|
if (text === "/settings") {
|
|
1053
|
-
pushMessage("system", `Settings: mode=${runtimeMode} scope=${workspaceRoot} model=${activeModel} provider=${getProviderForModel(activeModel)}
|
|
1192
|
+
pushMessage("system", `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()}`);
|
|
1054
1193
|
pushMessage("system", "");
|
|
1055
1194
|
return;
|
|
1056
1195
|
}
|
|
1057
|
-
if (text === "/voice") {
|
|
1196
|
+
if (text === "/dictate" || text === "/voice") {
|
|
1058
1197
|
if (!voiceEnabled) {
|
|
1059
1198
|
setVoiceEnabled(true);
|
|
1060
1199
|
startLiveVoice(true);
|
|
1061
1200
|
return;
|
|
1062
1201
|
}
|
|
1063
|
-
pushMessage("system", `
|
|
1202
|
+
pushMessage("system", `Dictation is on${voiceListening ? " (currently listening)" : ""}. Use /dictate off to disable.`);
|
|
1064
1203
|
pushMessage("system", "");
|
|
1065
1204
|
return;
|
|
1066
1205
|
}
|
|
@@ -1081,29 +1220,29 @@ export const AstraApp = () => {
|
|
|
1081
1220
|
await openHistory();
|
|
1082
1221
|
return;
|
|
1083
1222
|
}
|
|
1084
|
-
if (text === "/voice status") {
|
|
1085
|
-
pushMessage("system", `
|
|
1223
|
+
if (text === "/dictate status" || text === "/voice status") {
|
|
1224
|
+
pushMessage("system", `Dictation is ${voiceEnabled ? "on" : "off"}${voicePreparing ? " (preparing mic)" : ""}${voiceListening ? " (listening)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`);
|
|
1086
1225
|
pushMessage("system", "");
|
|
1087
1226
|
return;
|
|
1088
1227
|
}
|
|
1089
|
-
if (text === "/voice on") {
|
|
1228
|
+
if (text === "/dictate on" || text === "/voice on") {
|
|
1090
1229
|
setVoiceEnabled(true);
|
|
1091
1230
|
startLiveVoice(true);
|
|
1092
|
-
pushMessage("system", `
|
|
1231
|
+
pushMessage("system", `Dictation enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
|
|
1093
1232
|
pushMessage("system", "");
|
|
1094
1233
|
return;
|
|
1095
1234
|
}
|
|
1096
|
-
if (text === "/voice off") {
|
|
1235
|
+
if (text === "/dictate off" || text === "/voice off") {
|
|
1097
1236
|
await stopLiveVoice();
|
|
1098
1237
|
setVoiceEnabled(false);
|
|
1099
|
-
pushMessage("system", "
|
|
1238
|
+
pushMessage("system", "Dictation disabled.");
|
|
1100
1239
|
pushMessage("system", "");
|
|
1101
1240
|
return;
|
|
1102
1241
|
}
|
|
1103
|
-
if (text === "/voice input") {
|
|
1242
|
+
if (text === "/dictate input" || text === "/voice input") {
|
|
1104
1243
|
const transcribed = await transcribeOnce();
|
|
1105
1244
|
if (!transcribed) {
|
|
1106
|
-
pushMessage("error", "No speech transcribed. Ensure you're signed in and
|
|
1245
|
+
pushMessage("error", "No speech transcribed. Ensure you're signed in and your microphone capture works (optional ASTRA_STT_CAPTURE_COMMAND).");
|
|
1107
1246
|
return;
|
|
1108
1247
|
}
|
|
1109
1248
|
setVoiceQueuedPrompt(transcribed.trim());
|
|
@@ -1170,6 +1309,9 @@ export const AstraApp = () => {
|
|
|
1170
1309
|
localActionConfirmed = true;
|
|
1171
1310
|
}
|
|
1172
1311
|
}
|
|
1312
|
+
if (event.type === "done") {
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1173
1315
|
const piece = await handleEvent(event, activeSessionId);
|
|
1174
1316
|
if (piece) {
|
|
1175
1317
|
assistant += piece;
|
|
@@ -1209,9 +1351,11 @@ export const AstraApp = () => {
|
|
|
1209
1351
|
startLiveVoice,
|
|
1210
1352
|
stopLiveVoice,
|
|
1211
1353
|
thinking,
|
|
1354
|
+
toolFeedMode,
|
|
1212
1355
|
user,
|
|
1213
1356
|
voiceEnabled,
|
|
1214
1357
|
voiceListening,
|
|
1358
|
+
voicePreparing,
|
|
1215
1359
|
voiceWaitingForSilence,
|
|
1216
1360
|
workspaceRoot
|
|
1217
1361
|
]);
|
|
@@ -1224,7 +1368,26 @@ export const AstraApp = () => {
|
|
|
1224
1368
|
if (!queued) {
|
|
1225
1369
|
return;
|
|
1226
1370
|
}
|
|
1227
|
-
|
|
1371
|
+
const normalizedQueued = queued.toLowerCase();
|
|
1372
|
+
const last = lastVoicePromptRef.current;
|
|
1373
|
+
const isLikelyNoise = normalizedQueued.length < VOICE_MIN_CHARS || VOICE_NOISE_WORDS.has(normalizedQueued);
|
|
1374
|
+
const isFastDuplicate = last !== null &&
|
|
1375
|
+
last.text === normalizedQueued &&
|
|
1376
|
+
Date.now() - last.at <= VOICE_DUPLICATE_WINDOW_MS;
|
|
1377
|
+
if (isLikelyNoise || isFastDuplicate) {
|
|
1378
|
+
const now = Date.now();
|
|
1379
|
+
const lastIgnored = lastIgnoredVoiceRef.current;
|
|
1380
|
+
const shouldLogIgnored = !lastIgnored ||
|
|
1381
|
+
lastIgnored.text !== normalizedQueued ||
|
|
1382
|
+
now - lastIgnored.at > VOICE_DUPLICATE_WINDOW_MS;
|
|
1383
|
+
if (shouldLogIgnored) {
|
|
1384
|
+
pushMessage("system", `Ignored likely-noise dictation input: "${queued}"`);
|
|
1385
|
+
lastIgnoredVoiceRef.current = { text: normalizedQueued, at: now };
|
|
1386
|
+
}
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
lastVoicePromptRef.current = { text: normalizedQueued, at: Date.now() };
|
|
1390
|
+
pushMessage("system", `Dictation input: ${queued}`);
|
|
1228
1391
|
setPrompt("");
|
|
1229
1392
|
void sendPrompt(queued);
|
|
1230
1393
|
}, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
|
|
@@ -1232,13 +1395,13 @@ export const AstraApp = () => {
|
|
|
1232
1395
|
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" }) })] }));
|
|
1233
1396
|
}
|
|
1234
1397
|
if (booting) {
|
|
1235
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children:
|
|
1398
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsxs(Text, { color: "#8aa2c9", children: [_jsx(Spinner, { type: "dots12" }), " Booting Astra terminal shell..."] })] }));
|
|
1236
1399
|
}
|
|
1237
1400
|
if (bootError) {
|
|
1238
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children:
|
|
1401
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsx(Text, { color: "red", children: bootError })] }));
|
|
1239
1402
|
}
|
|
1240
1403
|
if (!user) {
|
|
1241
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children:
|
|
1404
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsxs(Text, { color: "#b8c8ff", children: ["Astra terminal AI pair programmer (", loginMode === "login" ? "Sign in" : "Create account", ")"] }), _jsx(Text, { color: "#7c8ea8", children: "Press Ctrl+T to toggle Sign in / Create account" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "#93a3b8", children: "Email: " }), _jsx(TextInput, { value: email, onChange: setEmail, focus: loginField === "email", onSubmit: () => {
|
|
1242
1405
|
setLoginField("password");
|
|
1243
1406
|
} })] }), _jsxs(Box, { children: [_jsx(Text, { color: "#93a3b8", children: "Password: " }), _jsx(TextInput, { value: password, onChange: setPassword, mask: "*", focus: loginField === "password", onSubmit: () => {
|
|
1244
1407
|
void doAuth();
|
|
@@ -1246,21 +1409,36 @@ export const AstraApp = () => {
|
|
|
1246
1409
|
}
|
|
1247
1410
|
if (historyOpen) {
|
|
1248
1411
|
const selected = filteredHistory[historyIndex];
|
|
1249
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children:
|
|
1412
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsx(Text, { color: "#2a3a50", children: divider }), historyMode === "picker" ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: "#dce9ff", children: "History Picker" }), _jsx(Text, { color: "#5a7a9a", children: "Esc close \u00B7 Enter select" })] }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: historyPickerIndex === 0 ? "#dce9ff" : "#7a9bba", children: [historyPickerIndex === 0 ? "❯ " : " ", "View chat history"] }) }), _jsx(Box, { marginTop: 1, flexDirection: "row", children: _jsxs(Text, { color: historyPickerIndex === 1 ? "#dce9ff" : "#7a9bba", children: [historyPickerIndex === 1 ? "❯ " : " ", "View credit usage history"] }) })] }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsxs(Text, { color: "#5a7a9a", children: ["Credit usage history opens: ", HISTORY_SETTINGS_URL] })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: "#dce9ff", children: "Chat History" }), _jsx(Text, { color: "#5a7a9a", children: "Esc back \u00B7 Enter open \u00B7 D delete \u00B7 \u2190/\u2192 page" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "search " }), _jsx(TextInput, { value: historyQuery, onChange: setHistoryQuery, placeholder: "Filter chats..." })] }), _jsx(Text, { color: "#2a3a50", children: divider }), historyLoading ? (_jsxs(Text, { color: "#8aa2c9", children: [_jsx(Spinner, { type: "dots12" }), " Loading chat history..."] })) : filteredHistory.length === 0 ? (_jsx(Text, { color: "#8ea1bd", children: "No sessions found." })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: pageRows.map((row, localIdx) => {
|
|
1250
1413
|
const idx = pageStart + localIdx;
|
|
1251
1414
|
const active = idx === historyIndex;
|
|
1252
1415
|
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));
|
|
1253
|
-
}) })), _jsx(Text, { color: "#2a3a50", children:
|
|
1416
|
+
}) })), _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 : "--"] })] })] }))] }));
|
|
1254
1417
|
}
|
|
1255
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children:
|
|
1418
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#7aa2ff", children: brand }), _jsx(Text, { color: "#8ea1bd", children: welcomeLine }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsx(Text, { color: "#7a9bba", children: `mode ${runtimeMode} · provider ${getProviderForModel(activeModel)} · model ${activeModel} · credits ${creditsRemaining ?? "--"}${lastCreditCost !== null ? ` (-${lastCreditCost})` : ""}` }), _jsx(Text, { color: "#6c88a8", children: `scope ${workspaceRoot.length > HEADER_PATH_MAX ? `…${workspaceRoot.slice(-(HEADER_PATH_MAX - 1))}` : workspaceRoot} · dictate ${voiceEnabled
|
|
1419
|
+
? voicePreparing
|
|
1420
|
+
? "on/preparing"
|
|
1421
|
+
: voiceListening
|
|
1422
|
+
? voiceWaitingForSilence
|
|
1423
|
+
? "on/waiting"
|
|
1424
|
+
: "on/listening"
|
|
1425
|
+
: "on/standby"
|
|
1426
|
+
: "off"}` }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsx(Text, { color: "#3a5068", children: "/help /new /history /dictate /dictate on|off|status" }), _jsx(Text, { color: "#3a5068", children: "/tools compact|expanded /settings /logout /exit" }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [messages.map((message, index) => {
|
|
1427
|
+
const prev = index > 0 ? messages[index - 1] : null;
|
|
1256
1428
|
const style = styleForKind(message.kind);
|
|
1257
1429
|
const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
|
|
1258
1430
|
const isSpacing = message.text === "" && message.kind === "system";
|
|
1431
|
+
const needsGroupGap = Boolean(prev) &&
|
|
1432
|
+
prev?.kind !== "system" &&
|
|
1433
|
+
message.kind !== "system" &&
|
|
1434
|
+
prev?.kind !== message.kind &&
|
|
1435
|
+
(message.kind === "assistant" || message.kind === "tool");
|
|
1259
1436
|
if (isSpacing) {
|
|
1260
1437
|
return _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: " " }) }, `${index}-spacer`);
|
|
1261
1438
|
}
|
|
1262
1439
|
if (message.kind === "tool" && message.card && !isSuperAdmin) {
|
|
1263
1440
|
const card = message.card;
|
|
1441
|
+
const localityLabel = card.locality === "LOCAL" ? "local" : "cloud";
|
|
1264
1442
|
const icon = card.kind === "error"
|
|
1265
1443
|
? "✕"
|
|
1266
1444
|
: card.kind === "fileDelete"
|
|
@@ -1287,16 +1465,16 @@ export const AstraApp = () => {
|
|
|
1287
1465
|
: card.kind === "preview"
|
|
1288
1466
|
? "#9ad5ff"
|
|
1289
1467
|
: "#9bc5ff";
|
|
1290
|
-
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,
|
|
1468
|
+
return (_jsxs(React.Fragment, { children: [needsGroupGap ? _jsx(Text, { children: " " }) : null, _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, card.count && card.count > 1 ? _jsxs(Text, { color: "#9cb8d8", children: [" (x", card.count, ")"] }) : null, _jsxs(Text, { color: "#5a7a9a", children: [" \u00B7 ", localityLabel] })] }), toolFeedMode === "expanded" ? (_jsxs(_Fragment, { children: [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}`)))] })) : null] })] })] }, `${index}-${message.kind}`));
|
|
1291
1469
|
}
|
|
1292
|
-
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}`));
|
|
1293
|
-
}), 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:
|
|
1470
|
+
return (_jsxs(React.Fragment, { children: [needsGroupGap ? _jsx(Text, { children: " " }) : null, _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}`));
|
|
1471
|
+
}), 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: "🎤 dictate".padEnd(LABEL_WIDTH, " ") }), voicePreparing ? (_jsx(Text, { color: "#f4d58a", children: "\uD83D\uDFE1 preparing microphone..." })) : voiceListening && !voiceWaitingForSilence ? (_jsx(Text, { color: "#9de3b4", children: "\uD83D\uDFE2 listening now - speak clearly" })) : voiceWaitingForSilence ? (_jsx(Text, { color: "#b7c4d8", children: "\u23F3 speech detected - waiting for silence to send" })) : (_jsx(Text, { color: "#6f8199", children: "\u26AA voice armed - preparing next listen window" }))] })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: "\u276F " }), _jsx(TextInput, { value: prompt, onSubmit: (value) => {
|
|
1294
1472
|
setPrompt("");
|
|
1295
1473
|
void sendPrompt(value);
|
|
1296
1474
|
}, onChange: (value) => {
|
|
1297
1475
|
if (!voiceListening) {
|
|
1298
1476
|
setPrompt(value);
|
|
1299
1477
|
}
|
|
1300
|
-
}, placeholder: voiceEnabled ? "Ask Astra... (
|
|
1478
|
+
}, placeholder: voiceEnabled ? "Ask Astra... (dictate on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
|
|
1301
1479
|
};
|
|
1302
1480
|
//# sourceMappingURL=App.js.map
|