@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 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
  }
@@ -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: (event.data ?? {}).local === true ? "LOCAL" : "REMOTE",
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 /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.");
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)} 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()}`);
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", `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.`);
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", `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)" : ""}.`);
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", `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.`);
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", "Voice input disabled.");
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 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).");
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
- 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}`);
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: 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..."] })] }));
1234
1399
  }
1235
1400
  if (bootError) {
1236
- 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 })] }));
1237
1402
  }
1238
1403
  if (!user) {
1239
- 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: () => {
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: 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) => {
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: 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 : "--"] })] })] }))] }));
1252
1417
  }
1253
- 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;
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, " ", _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}`));
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: 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) => {
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... (voice on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
1478
+ }, placeholder: voiceEnabled ? "Ask Astra... (dictate on: auto listen + send on silence)" : "Ask Astra..." })] })] }));
1299
1479
  };
1300
1480
  //# sourceMappingURL=App.js.map