@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 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
- return input
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
- .trim();
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) => [...prev, { kind: "tool", text: card.summary, card }].slice(-300));
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
- setVoiceListening(true);
600
+ setVoicePreparing(true);
601
+ setVoiceListening(false);
505
602
  setVoiceWaitingForSilence(false);
506
603
  if (announce) {
507
- pushMessage("system", "Voice input started. Speak now…");
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: (event.data ?? {}).local === true ? "LOCAL" : "REMOTE",
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 /voice /voice on|off|status /settings /settings model <id> /logout /exit");
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)} voice=${voiceEnabled ? "on" : "off"} listening=${voiceListening ? "yes" : "no"} silence_ms=${VOICE_SILENCE_MS} role=${user.role ?? "user"} client_id=${getDefaultClientId()} backend=${getBackendUrl()}`);
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", `Voice input is on${voiceListening ? " (currently listening)" : ""}. Use /voice off to disable.`);
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", `Voice input is ${voiceEnabled ? "on" : "off"}${voiceListening ? " (live transcription active)" : ""}${voiceWaitingForSilence ? " (waiting for silence to auto-send)" : ""}.`);
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", `Voice input enabled. Auto-send after ${Math.round(VOICE_SILENCE_MS / 1000)}s silence.`);
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", "Voice input disabled.");
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 that mic capture works (optional ASTRA_STT_CAPTURE_COMMAND).");
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
- pushMessage("system", `Voice input: ${queued}`);
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: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsxs(Text, { color: "#8aa2c9", children: [_jsx(Spinner, { type: "dots12" }), " Booting Astra terminal shell..."] })] }));
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: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsx(Text, { color: "red", children: bootError })] }));
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: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _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: () => {
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: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _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) => {
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: 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 : "--"] })] })] }))] }));
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: ASTRA_ASCII }), _jsx(Text, { color: "#8ea1bd", children: FOUNDER_WELCOME }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "mode " }), _jsx(Text, { color: "#9ad5ff", children: runtimeMode })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "scope " }), _jsx(Text, { color: "#7a9bba", children: workspaceRoot })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "provider " }), _jsx(Text, { color: "#9ad5ff", children: getProviderForModel(activeModel) })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "credits " }), _jsxs(Text, { color: creditsRemaining !== null && creditsRemaining < 50 ? "#ffaa55" : "#9ad5ff", children: [creditsRemaining ?? "--", lastCreditCost !== null ? (_jsxs(Text, { color: "#5a7a9a", children: [" (-", lastCreditCost, ")"] })) : null] })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "model " }), _jsx(Text, { color: "#9ad5ff", children: activeModel })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "#4a6070", children: "voice " }), _jsx(Text, { color: voiceEnabled ? "#9ad5ff" : "#5a7a9a", children: voiceEnabled ? (voiceListening ? (voiceWaitingForSilence ? "on/waiting" : "on/listening") : "on") : "off" })] })] }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsx(Text, { color: "#3a5068", children: "/help /new /history /voice /voice on|off|status /settings /logout /exit" }), _jsx(Text, { color: "#2a3a50", children: DIVIDER }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [messages.map((message, index) => {
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, " ", _jsxs(Text, { color: "#5a7a9a", children: ["[", card.locality, "]"] })] }), card.path ? _jsxs(Text, { color: "#6c88a8", children: ["path: ", card.path] }) : null, (card.snippetLines ?? []).slice(0, TOOL_SNIPPET_LINES).map((line, idx) => (_jsx(Text, { color: "#8ea1bd", children: line }, `${index}-snippet-${idx}`)))] })] }, `${index}-${message.kind}`));
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: DIVIDER }), thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#7aa2ff", children: "◆ astra".padEnd(LABEL_WIDTH, " ") }), _jsxs(Text, { color: "#6080a0", children: [_jsx(Spinner, { type: "dots2" }), _jsx(Text, { color: "#8aa2c9", children: " thinking..." })] })] })) : null, voiceEnabled && !thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#9ad5ff", children: "🎤 voice".padEnd(LABEL_WIDTH, " ") }), voiceListening && !voiceWaitingForSilence ? (_jsx(Text, { color: "#a6d9ff", children: "\uD83C\uDF99 listening... goblin ears activated \uD83D\uDC42" })) : voiceWaitingForSilence ? (_jsx(Text, { color: "#b7c4d8", children: "\u23F8 waiting for silence... dramatic pause loading..." })) : (_jsx(Text, { color: "#6f8199", children: "voice armed... say something when ready" }))] })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "#7aa2ff", children: "\u276F " }), _jsx(TextInput, { value: prompt, onSubmit: (value) => {
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... (voice on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
1478
+ }, placeholder: voiceEnabled ? "Ask Astra... (dictate on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
1301
1479
  };
1302
1480
  //# sourceMappingURL=App.js.map