@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/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
|
}
|
|
@@ -749,6 +858,8 @@ export const AstraApp = () => {
|
|
|
749
858
|
throw new Error(data.error);
|
|
750
859
|
}
|
|
751
860
|
const authSession = data;
|
|
861
|
+
// Set token immediately so follow-up profile/session calls are authenticated.
|
|
862
|
+
backend.setAuthSession(authSession);
|
|
752
863
|
const hydrated = await backend.getUserProfile(authSession).catch(() => authSession);
|
|
753
864
|
saveSession(hydrated);
|
|
754
865
|
backend.setAuthSession(hydrated);
|
|
@@ -993,19 +1104,26 @@ export const AstraApp = () => {
|
|
|
993
1104
|
}
|
|
994
1105
|
if (event.type === "credits_exhausted") {
|
|
995
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;
|
|
996
1122
|
}
|
|
997
1123
|
if (!isSuperAdmin && NOISY_EVENT_TYPES.has(event.type)) {
|
|
998
1124
|
return null;
|
|
999
1125
|
}
|
|
1000
1126
|
if (!isSuperAdmin && event.type === "tool_start") {
|
|
1001
|
-
const tool = event.tool;
|
|
1002
|
-
const name = tool?.name ?? "tool";
|
|
1003
|
-
pushToolCard({
|
|
1004
|
-
kind: "start",
|
|
1005
|
-
toolName: name,
|
|
1006
|
-
locality: "REMOTE",
|
|
1007
|
-
summary: `${name} is running...`
|
|
1008
|
-
});
|
|
1009
1127
|
return null;
|
|
1010
1128
|
}
|
|
1011
1129
|
const toolLine = eventToToolLine(event);
|
|
@@ -1020,11 +1138,22 @@ export const AstraApp = () => {
|
|
|
1020
1138
|
else if (event.type === "tool_result") {
|
|
1021
1139
|
const mark = event.success ? "completed" : "failed";
|
|
1022
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));
|
|
1023
1152
|
pushToolCard({
|
|
1024
1153
|
kind: event.success ? "success" : "error",
|
|
1025
1154
|
toolName,
|
|
1026
|
-
locality:
|
|
1027
|
-
summary: `${toolName} ${mark}`
|
|
1155
|
+
locality: payload.local === true ? "LOCAL" : "REMOTE",
|
|
1156
|
+
summary: event.success ? summary : `${toolName} ${mark}`
|
|
1028
1157
|
});
|
|
1029
1158
|
}
|
|
1030
1159
|
}
|
|
@@ -1036,29 +1165,41 @@ export const AstraApp = () => {
|
|
|
1036
1165
|
}
|
|
1037
1166
|
}
|
|
1038
1167
|
return null;
|
|
1039
|
-
}, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, writeLocalFile]);
|
|
1168
|
+
}, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, stopLiveVoice, voiceEnabled, writeLocalFile]);
|
|
1040
1169
|
const sendPrompt = useCallback(async (rawPrompt) => {
|
|
1041
1170
|
const text = rawPrompt.trim();
|
|
1042
1171
|
if (!text || !user || thinking) {
|
|
1043
1172
|
return;
|
|
1044
1173
|
}
|
|
1045
1174
|
if (text === "/help") {
|
|
1046
|
-
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.");
|
|
1047
1188
|
pushMessage("system", "");
|
|
1048
1189
|
return;
|
|
1049
1190
|
}
|
|
1050
1191
|
if (text === "/settings") {
|
|
1051
|
-
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()}`);
|
|
1052
1193
|
pushMessage("system", "");
|
|
1053
1194
|
return;
|
|
1054
1195
|
}
|
|
1055
|
-
if (text === "/voice") {
|
|
1196
|
+
if (text === "/dictate" || text === "/voice") {
|
|
1056
1197
|
if (!voiceEnabled) {
|
|
1057
1198
|
setVoiceEnabled(true);
|
|
1058
1199
|
startLiveVoice(true);
|
|
1059
1200
|
return;
|
|
1060
1201
|
}
|
|
1061
|
-
pushMessage("system", `
|
|
1202
|
+
pushMessage("system", `Dictation is on${voiceListening ? " (currently listening)" : ""}. Use /dictate off to disable.`);
|
|
1062
1203
|
pushMessage("system", "");
|
|
1063
1204
|
return;
|
|
1064
1205
|
}
|
|
@@ -1079,29 +1220,29 @@ export const AstraApp = () => {
|
|
|
1079
1220
|
await openHistory();
|
|
1080
1221
|
return;
|
|
1081
1222
|
}
|
|
1082
|
-
if (text === "/voice status") {
|
|
1083
|
-
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)" : ""}.`);
|
|
1084
1225
|
pushMessage("system", "");
|
|
1085
1226
|
return;
|
|
1086
1227
|
}
|
|
1087
|
-
if (text === "/voice on") {
|
|
1228
|
+
if (text === "/dictate on" || text === "/voice on") {
|
|
1088
1229
|
setVoiceEnabled(true);
|
|
1089
1230
|
startLiveVoice(true);
|
|
1090
|
-
pushMessage("system", `
|
|
1231
|
+
pushMessage("system", `Dictation enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
|
|
1091
1232
|
pushMessage("system", "");
|
|
1092
1233
|
return;
|
|
1093
1234
|
}
|
|
1094
|
-
if (text === "/voice off") {
|
|
1235
|
+
if (text === "/dictate off" || text === "/voice off") {
|
|
1095
1236
|
await stopLiveVoice();
|
|
1096
1237
|
setVoiceEnabled(false);
|
|
1097
|
-
pushMessage("system", "
|
|
1238
|
+
pushMessage("system", "Dictation disabled.");
|
|
1098
1239
|
pushMessage("system", "");
|
|
1099
1240
|
return;
|
|
1100
1241
|
}
|
|
1101
|
-
if (text === "/voice input") {
|
|
1242
|
+
if (text === "/dictate input" || text === "/voice input") {
|
|
1102
1243
|
const transcribed = await transcribeOnce();
|
|
1103
1244
|
if (!transcribed) {
|
|
1104
|
-
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).");
|
|
1105
1246
|
return;
|
|
1106
1247
|
}
|
|
1107
1248
|
setVoiceQueuedPrompt(transcribed.trim());
|
|
@@ -1168,6 +1309,9 @@ export const AstraApp = () => {
|
|
|
1168
1309
|
localActionConfirmed = true;
|
|
1169
1310
|
}
|
|
1170
1311
|
}
|
|
1312
|
+
if (event.type === "done") {
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1171
1315
|
const piece = await handleEvent(event, activeSessionId);
|
|
1172
1316
|
if (piece) {
|
|
1173
1317
|
assistant += piece;
|
|
@@ -1207,9 +1351,11 @@ export const AstraApp = () => {
|
|
|
1207
1351
|
startLiveVoice,
|
|
1208
1352
|
stopLiveVoice,
|
|
1209
1353
|
thinking,
|
|
1354
|
+
toolFeedMode,
|
|
1210
1355
|
user,
|
|
1211
1356
|
voiceEnabled,
|
|
1212
1357
|
voiceListening,
|
|
1358
|
+
voicePreparing,
|
|
1213
1359
|
voiceWaitingForSilence,
|
|
1214
1360
|
workspaceRoot
|
|
1215
1361
|
]);
|
|
@@ -1222,7 +1368,26 @@ export const AstraApp = () => {
|
|
|
1222
1368
|
if (!queued) {
|
|
1223
1369
|
return;
|
|
1224
1370
|
}
|
|
1225
|
-
|
|
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}`);
|
|
1226
1391
|
setPrompt("");
|
|
1227
1392
|
void sendPrompt(queued);
|
|
1228
1393
|
}, [pushMessage, sendPrompt, thinking, user, voiceQueuedPrompt]);
|
|
@@ -1230,13 +1395,13 @@ export const AstraApp = () => {
|
|
|
1230
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" }) })] }));
|
|
1231
1396
|
}
|
|
1232
1397
|
if (booting) {
|
|
1233
|
-
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..."] })] }));
|
|
1234
1399
|
}
|
|
1235
1400
|
if (bootError) {
|
|
1236
|
-
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 })] }));
|
|
1237
1402
|
}
|
|
1238
1403
|
if (!user) {
|
|
1239
|
-
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: () => {
|
|
1240
1405
|
setLoginField("password");
|
|
1241
1406
|
} })] }), _jsxs(Box, { children: [_jsx(Text, { color: "#93a3b8", children: "Password: " }), _jsx(TextInput, { value: password, onChange: setPassword, mask: "*", focus: loginField === "password", onSubmit: () => {
|
|
1242
1407
|
void doAuth();
|
|
@@ -1244,21 +1409,36 @@ export const AstraApp = () => {
|
|
|
1244
1409
|
}
|
|
1245
1410
|
if (historyOpen) {
|
|
1246
1411
|
const selected = filteredHistory[historyIndex];
|
|
1247
|
-
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) => {
|
|
1248
1413
|
const idx = pageStart + localIdx;
|
|
1249
1414
|
const active = idx === historyIndex;
|
|
1250
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));
|
|
1251
|
-
}) })), _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 : "--"] })] })] }))] }));
|
|
1252
1417
|
}
|
|
1253
|
-
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;
|
|
1254
1428
|
const style = styleForKind(message.kind);
|
|
1255
1429
|
const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
|
|
1256
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");
|
|
1257
1436
|
if (isSpacing) {
|
|
1258
1437
|
return _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: " " }) }, `${index}-spacer`);
|
|
1259
1438
|
}
|
|
1260
1439
|
if (message.kind === "tool" && message.card && !isSuperAdmin) {
|
|
1261
1440
|
const card = message.card;
|
|
1441
|
+
const localityLabel = card.locality === "LOCAL" ? "local" : "cloud";
|
|
1262
1442
|
const icon = card.kind === "error"
|
|
1263
1443
|
? "✕"
|
|
1264
1444
|
: card.kind === "fileDelete"
|
|
@@ -1285,16 +1465,16 @@ export const AstraApp = () => {
|
|
|
1285
1465
|
: card.kind === "preview"
|
|
1286
1466
|
? "#9ad5ff"
|
|
1287
1467
|
: "#9bc5ff";
|
|
1288
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, children: paddedLabel }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: accent, children: [icon, " ", card.summary,
|
|
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}`));
|
|
1289
1469
|
}
|
|
1290
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, bold: style.bold, children: paddedLabel }), message.kind === "assistant" ? (_jsx(Box, { flexDirection: "column", children: renderMarkdownContent(message.text, style.textColor, `assistant-${index}`) })) : (_jsx(Text, { color: style.textColor, bold: style.bold && message.kind === "error", children: message.text }))] }, `${index}-${message.kind}`));
|
|
1291
|
-
}), streamingText ? (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: styleForKind("assistant").label.padEnd(LABEL_WIDTH, " ") }), _jsx(Box, { flexDirection: "column", children: renderMarkdownContent(streamingText, "#dce9ff", "streaming") })] })) : null] }), _jsx(Text, { color: "#2a3a50", children:
|
|
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) => {
|
|
1292
1472
|
setPrompt("");
|
|
1293
1473
|
void sendPrompt(value);
|
|
1294
1474
|
}, onChange: (value) => {
|
|
1295
1475
|
if (!voiceListening) {
|
|
1296
1476
|
setPrompt(value);
|
|
1297
1477
|
}
|
|
1298
|
-
}, placeholder: voiceEnabled ? "Ask Astra... (
|
|
1478
|
+
}, placeholder: voiceEnabled ? "Ask Astra... (dictate on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
|
|
1299
1479
|
};
|
|
1300
1480
|
//# sourceMappingURL=App.js.map
|