@astra-code/astra-ai 0.1.6 → 0.1.7
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/dist/app/App.js +181 -47
- package/dist/app/App.js.map +1 -1
- package/dist/lib/backendClient.js +87 -17
- package/dist/lib/backendClient.js.map +1 -1
- package/package.json +1 -1
- package/src/app/App.tsx +229 -77
- package/src/lib/backendClient.ts +88 -17
- package/src/types/events.ts +1 -0
package/dist/app/App.js
CHANGED
|
@@ -151,6 +151,14 @@ const extractAssistantText = (event) => {
|
|
|
151
151
|
};
|
|
152
152
|
const LABEL_WIDTH = 10;
|
|
153
153
|
const HEADER_PATH_MAX = Number(process.env.ASTRA_HEADER_PATH_MAX ?? "42");
|
|
154
|
+
const formatTime = (ts) => {
|
|
155
|
+
const d = new Date(ts);
|
|
156
|
+
const raw = d.getHours();
|
|
157
|
+
const ampm = raw >= 12 ? "pm" : "am";
|
|
158
|
+
const h = raw % 12 === 0 ? 12 : raw % 12;
|
|
159
|
+
const m = d.getMinutes().toString().padStart(2, "0");
|
|
160
|
+
return `${h}:${m} ${ampm}`;
|
|
161
|
+
};
|
|
154
162
|
const MIN_DIVIDER = 64;
|
|
155
163
|
const MAX_DIVIDER = 120;
|
|
156
164
|
const styleForKind = (kind) => {
|
|
@@ -193,7 +201,13 @@ const normalizeAssistantText = (input) => {
|
|
|
193
201
|
// Remove control chars but preserve newlines/tabs.
|
|
194
202
|
.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, "")
|
|
195
203
|
.replace(/([a-z0-9/])([A-Z])/g, "$1 $2")
|
|
196
|
-
|
|
204
|
+
// Add space after sentence-ending punctuation only when followed by an
|
|
205
|
+
// uppercase letter (sentence start). Using [A-Za-z] here would break
|
|
206
|
+
// file extensions like .css, .json, .jsx, .tsx — those always start with
|
|
207
|
+
// a lowercase letter.
|
|
208
|
+
.replace(/([.!?])([A-Z])/g, "$1 $2")
|
|
209
|
+
// For ! and ? followed by lowercase, also add a space (natural English).
|
|
210
|
+
.replace(/([!?])([a-z])/g, "$1 $2")
|
|
197
211
|
.replace(/([a-z])(\u2022)/g, "$1\n$2")
|
|
198
212
|
.replace(/([^\n])(Summary:|Next:|Tests:)/g, "$1\n\n$2")
|
|
199
213
|
.replace(/([A-Za-z0-9_/-]+)\.\s+md\b/g, "$1.md")
|
|
@@ -220,34 +234,56 @@ const normalizeAssistantText = (input) => {
|
|
|
220
234
|
}
|
|
221
235
|
return deduped.join("\n\n").trim();
|
|
222
236
|
};
|
|
237
|
+
const guessDevUrl = (command) => {
|
|
238
|
+
// Extract an explicit --port or -p value from the command.
|
|
239
|
+
const portMatch = command.match(/(?:--port|-p)\s+(\d+)/) ?? command.match(/(?:--port=|-p=)(\d+)/);
|
|
240
|
+
if (portMatch) {
|
|
241
|
+
return `http://localhost:${portMatch[1]}`;
|
|
242
|
+
}
|
|
243
|
+
// Default ports by framework.
|
|
244
|
+
if (/next/.test(command))
|
|
245
|
+
return "http://localhost:3000";
|
|
246
|
+
if (/vite|vue/.test(command))
|
|
247
|
+
return "http://localhost:5173";
|
|
248
|
+
if (/remix/.test(command))
|
|
249
|
+
return "http://localhost:3000";
|
|
250
|
+
if (/astro/.test(command))
|
|
251
|
+
return "http://localhost:4321";
|
|
252
|
+
if (/angular|ng\s+serve/.test(command))
|
|
253
|
+
return "http://localhost:4200";
|
|
254
|
+
if (/npm\s+run\s+dev|npm\s+start|npx\s+react-scripts/.test(command))
|
|
255
|
+
return "http://localhost:3000";
|
|
256
|
+
return null;
|
|
257
|
+
};
|
|
223
258
|
const summarizeToolResult = (toolName, data, success) => {
|
|
224
259
|
if (!success) {
|
|
225
|
-
return `${toolName} failed
|
|
260
|
+
return { summary: `${toolName} failed` };
|
|
226
261
|
}
|
|
227
262
|
const path = typeof data.path === "string" ? data.path : "";
|
|
228
263
|
const totalLines = typeof data.total_lines === "number" ? data.total_lines : null;
|
|
229
264
|
if (toolName === "view_file") {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
if (path)
|
|
234
|
-
|
|
235
|
-
|
|
265
|
+
const result = {
|
|
266
|
+
summary: totalLines !== null ? `Read ${totalLines} lines` : "Read file",
|
|
267
|
+
};
|
|
268
|
+
if (path)
|
|
269
|
+
result.path = path;
|
|
270
|
+
return result;
|
|
236
271
|
}
|
|
237
272
|
if (toolName === "list_directory") {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
return `Listed <${dir}>`;
|
|
273
|
+
return {
|
|
274
|
+
summary: totalLines !== null ? `Listed ${totalLines} entries` : "Listed directory",
|
|
275
|
+
path: path || ".",
|
|
276
|
+
};
|
|
243
277
|
}
|
|
244
278
|
if (toolName === "semantic_search") {
|
|
245
|
-
return "Searched codebase context";
|
|
279
|
+
return { summary: "Searched codebase context" };
|
|
246
280
|
}
|
|
247
281
|
if (toolName === "search_files") {
|
|
248
|
-
return
|
|
282
|
+
return {
|
|
283
|
+
summary: totalLines !== null ? `Found ${totalLines} matching paths` : "Searched file paths",
|
|
284
|
+
};
|
|
249
285
|
}
|
|
250
|
-
return `${toolName} completed
|
|
286
|
+
return { summary: `${toolName} completed` };
|
|
251
287
|
};
|
|
252
288
|
const isLikelyVoiceNoise = (text) => {
|
|
253
289
|
const normalized = text.trim().toLowerCase();
|
|
@@ -442,9 +478,17 @@ export const AstraApp = () => {
|
|
|
442
478
|
const welcomeLine = dividerWidth < 96 ? "Welcome to Astra" : FOUNDER_WELCOME;
|
|
443
479
|
const backend = useMemo(() => new BackendClient(), []);
|
|
444
480
|
const { exit } = useApp();
|
|
481
|
+
// Keep backend token fresh: when the client auto-refreshes on 401, persist and update state.
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
backend.setOnTokenRefreshed((refreshed) => {
|
|
484
|
+
saveSession(refreshed);
|
|
485
|
+
setUser(refreshed);
|
|
486
|
+
});
|
|
487
|
+
}, [backend]);
|
|
445
488
|
// In-session file cache: tracks files created/edited so subsequent requests
|
|
446
489
|
// include their latest content in workspaceFiles (VirtualFS stays up to date).
|
|
447
490
|
const localFileCache = useRef(new Map());
|
|
491
|
+
const abortRunRef = useRef(null);
|
|
448
492
|
const writeLocalFile = useCallback((relPath, content, language) => {
|
|
449
493
|
try {
|
|
450
494
|
const abs = join(workspaceRoot, relPath);
|
|
@@ -483,6 +527,7 @@ export const AstraApp = () => {
|
|
|
483
527
|
const [activeModel, setActiveModel] = useState(getDefaultModel());
|
|
484
528
|
const [creditsRemaining, setCreditsRemaining] = useState(null);
|
|
485
529
|
const [lastCreditCost, setLastCreditCost] = useState(null);
|
|
530
|
+
const [loopCtx, setLoopCtx] = useState(null);
|
|
486
531
|
const runtimeMode = getRuntimeMode();
|
|
487
532
|
const [prompt, setPrompt] = useState("");
|
|
488
533
|
const [thinking, setThinking] = useState(false);
|
|
@@ -494,6 +539,7 @@ export const AstraApp = () => {
|
|
|
494
539
|
const [voiceQueuedPrompt, setVoiceQueuedPrompt] = useState(null);
|
|
495
540
|
const [micSetupDevices, setMicSetupDevices] = useState(null);
|
|
496
541
|
const [toolFeedMode, setToolFeedMode] = useState("compact");
|
|
542
|
+
const [thinkingColorIdx, setThinkingColorIdx] = useState(0);
|
|
497
543
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
498
544
|
const [historyMode, setHistoryMode] = useState("picker");
|
|
499
545
|
const [historyPickerIndex, setHistoryPickerIndex] = useState(0);
|
|
@@ -508,11 +554,11 @@ export const AstraApp = () => {
|
|
|
508
554
|
const fileEditBuffersRef = useRef(new Map());
|
|
509
555
|
const isSuperAdmin = user?.role === "super_admin";
|
|
510
556
|
const pushMessage = useCallback((kind, text) => {
|
|
511
|
-
setMessages((prev) => [...prev, { kind, text }].slice(-300));
|
|
557
|
+
setMessages((prev) => [...prev, { kind, text, ts: Date.now() }].slice(-300));
|
|
512
558
|
}, []);
|
|
513
559
|
const pushToolCard = useCallback((card) => {
|
|
514
560
|
setMessages((prev) => {
|
|
515
|
-
const nextEntry = { kind: "tool", text: card.summary, card };
|
|
561
|
+
const nextEntry = { kind: "tool", text: card.summary, card, ts: Date.now() };
|
|
516
562
|
const last = prev[prev.length - 1];
|
|
517
563
|
if (last &&
|
|
518
564
|
last.kind === "tool" &&
|
|
@@ -806,6 +852,12 @@ export const AstraApp = () => {
|
|
|
806
852
|
exit();
|
|
807
853
|
return;
|
|
808
854
|
}
|
|
855
|
+
if (key.escape && thinking) {
|
|
856
|
+
abortRunRef.current?.abort();
|
|
857
|
+
pushMessage("system", "Cancelled.");
|
|
858
|
+
pushMessage("system", "");
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
809
861
|
if (historyOpen) {
|
|
810
862
|
if (key.escape) {
|
|
811
863
|
if (historyMode === "sessions") {
|
|
@@ -1122,16 +1174,18 @@ export const AstraApp = () => {
|
|
|
1122
1174
|
}
|
|
1123
1175
|
}
|
|
1124
1176
|
if (!isSuperAdmin && event.tool_name === "start_preview") {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1177
|
+
// Server mode returns preview_url (tunnel). Desktop mode returns a
|
|
1178
|
+
// plain message — try to guess the local URL from the command.
|
|
1179
|
+
const tunnelUrl = typeof d.preview_url === "string" ? d.preview_url : null;
|
|
1180
|
+
const command = typeof d.command === "string" ? d.command : "";
|
|
1181
|
+
const localUrl = !tunnelUrl ? guessDevUrl(command) : null;
|
|
1182
|
+
const displayUrl = tunnelUrl ?? localUrl;
|
|
1130
1183
|
pushToolCard({
|
|
1131
1184
|
kind: "preview",
|
|
1132
1185
|
toolName: "start_preview",
|
|
1133
1186
|
locality,
|
|
1134
|
-
summary:
|
|
1187
|
+
summary: "Dev server running",
|
|
1188
|
+
...(displayUrl ? { path: displayUrl } : {}),
|
|
1135
1189
|
});
|
|
1136
1190
|
return null;
|
|
1137
1191
|
}
|
|
@@ -1176,6 +1230,16 @@ export const AstraApp = () => {
|
|
|
1176
1230
|
pushMessage("system", "");
|
|
1177
1231
|
}
|
|
1178
1232
|
}
|
|
1233
|
+
if (event.type === "timing") {
|
|
1234
|
+
const ev = event;
|
|
1235
|
+
if (ev.phase === "llm_done") {
|
|
1236
|
+
const inTok = Number(ev.input_tokens ?? 0);
|
|
1237
|
+
const outTok = Number(ev.output_tokens ?? 0);
|
|
1238
|
+
if (inTok > 0 || outTok > 0)
|
|
1239
|
+
setLoopCtx({ in: inTok, out: outTok });
|
|
1240
|
+
}
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1179
1243
|
if (event.type === "continuation_check") {
|
|
1180
1244
|
const recommendation = typeof event.recommendation === "string" && event.recommendation
|
|
1181
1245
|
? event.recommendation
|
|
@@ -1213,12 +1277,13 @@ export const AstraApp = () => {
|
|
|
1213
1277
|
if (alreadyRepresented) {
|
|
1214
1278
|
return null;
|
|
1215
1279
|
}
|
|
1216
|
-
const summary = summarizeToolResult(toolName, payload, Boolean(event.success));
|
|
1280
|
+
const { summary, path: summaryPath } = summarizeToolResult(toolName, payload, Boolean(event.success));
|
|
1217
1281
|
pushToolCard({
|
|
1218
1282
|
kind: event.success ? "success" : "error",
|
|
1219
1283
|
toolName,
|
|
1220
1284
|
locality: payload.local === true ? "LOCAL" : "REMOTE",
|
|
1221
|
-
summary: event.success ? summary : `${toolName} ${mark}
|
|
1285
|
+
summary: event.success ? summary : `${toolName} ${mark}`,
|
|
1286
|
+
...(summaryPath ? { path: summaryPath } : {}),
|
|
1222
1287
|
});
|
|
1223
1288
|
}
|
|
1224
1289
|
}
|
|
@@ -1230,12 +1295,13 @@ export const AstraApp = () => {
|
|
|
1230
1295
|
}
|
|
1231
1296
|
}
|
|
1232
1297
|
return null;
|
|
1233
|
-
}, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, stopLiveVoice, voiceEnabled, writeLocalFile]);
|
|
1298
|
+
}, [backend, deleteLocalFile, isSuperAdmin, pushMessage, pushToolCard, setLoopCtx, stopLiveVoice, voiceEnabled, writeLocalFile]);
|
|
1234
1299
|
const sendPrompt = useCallback(async (rawPrompt) => {
|
|
1235
1300
|
const text = rawPrompt.trim();
|
|
1236
1301
|
if (!text || !user || thinking) {
|
|
1237
1302
|
return;
|
|
1238
1303
|
}
|
|
1304
|
+
setPrompt("");
|
|
1239
1305
|
// Mic onboarding: intercept when waiting for device selection.
|
|
1240
1306
|
if (micSetupDevices !== null) {
|
|
1241
1307
|
const idx = parseInt(text, 10);
|
|
@@ -1258,17 +1324,29 @@ export const AstraApp = () => {
|
|
|
1258
1324
|
return;
|
|
1259
1325
|
}
|
|
1260
1326
|
if (text === "/help") {
|
|
1261
|
-
pushMessage("system",
|
|
1327
|
+
pushMessage("system", isSuperAdmin
|
|
1328
|
+
? "/new /history /voice /dictate on|off|status /tools compact|expanded /settings /settings model <id> /logout /exit"
|
|
1329
|
+
: "/new /history /voice /dictate on|off|status /settings /settings model <id> /logout /exit");
|
|
1262
1330
|
pushMessage("system", "");
|
|
1263
1331
|
return;
|
|
1264
1332
|
}
|
|
1265
1333
|
if (text === "/tools compact") {
|
|
1334
|
+
if (!isSuperAdmin) {
|
|
1335
|
+
pushMessage("system", "Unknown command. Type /help for available commands.");
|
|
1336
|
+
pushMessage("system", "");
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1266
1339
|
setToolFeedMode("compact");
|
|
1267
1340
|
pushMessage("system", "Tool feed set to compact.");
|
|
1268
1341
|
pushMessage("system", "");
|
|
1269
1342
|
return;
|
|
1270
1343
|
}
|
|
1271
1344
|
if (text === "/tools expanded") {
|
|
1345
|
+
if (!isSuperAdmin) {
|
|
1346
|
+
pushMessage("system", "Unknown command. Type /help for available commands.");
|
|
1347
|
+
pushMessage("system", "");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1272
1350
|
setToolFeedMode("expanded");
|
|
1273
1351
|
pushMessage("system", "Tool feed set to expanded.");
|
|
1274
1352
|
pushMessage("system", "");
|
|
@@ -1367,6 +1445,9 @@ export const AstraApp = () => {
|
|
|
1367
1445
|
pushMessage("user", text);
|
|
1368
1446
|
setThinking(true);
|
|
1369
1447
|
setStreamingText("");
|
|
1448
|
+
setLoopCtx(null);
|
|
1449
|
+
const abortController = new AbortController();
|
|
1450
|
+
abortRunRef.current = abortController;
|
|
1370
1451
|
try {
|
|
1371
1452
|
// Scan the local workspace so the backend VirtualFS is populated.
|
|
1372
1453
|
// Merge in any files created/edited during this session so edits
|
|
@@ -1375,8 +1456,25 @@ export const AstraApp = () => {
|
|
|
1375
1456
|
const sessionFiles = Array.from(localFileCache.current.values());
|
|
1376
1457
|
const seenPaths = new Set(sessionFiles.map((f) => f.path));
|
|
1377
1458
|
const mergedFiles = [...sessionFiles, ...scannedFiles.filter((f) => !seenPaths.has(f.path))];
|
|
1378
|
-
|
|
1459
|
+
// `pendingText` is text received since the last committed block.
|
|
1460
|
+
// It gets flushed to the messages list whenever tool activity starts,
|
|
1461
|
+
// keeping text and tool cards in the exact order they were emitted.
|
|
1462
|
+
let pendingText = "";
|
|
1463
|
+
let allAssistantText = "";
|
|
1379
1464
|
let localActionConfirmed = false;
|
|
1465
|
+
const commitPending = (applyGuard = false) => {
|
|
1466
|
+
if (!pendingText.trim()) {
|
|
1467
|
+
pendingText = "";
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const clean = normalizeAssistantText(pendingText);
|
|
1471
|
+
const msg = applyGuard && !localActionConfirmed && looksLikeLocalFilesystemClaim(clean)
|
|
1472
|
+
? `Remote result (not yet confirmed as local filesystem change): ${clean}`
|
|
1473
|
+
: clean;
|
|
1474
|
+
pushMessage("assistant", msg);
|
|
1475
|
+
pendingText = "";
|
|
1476
|
+
setStreamingText("");
|
|
1477
|
+
};
|
|
1380
1478
|
for await (const event of backend.streamChat({
|
|
1381
1479
|
user,
|
|
1382
1480
|
sessionId: activeSessionId,
|
|
@@ -1384,7 +1482,8 @@ export const AstraApp = () => {
|
|
|
1384
1482
|
workspaceRoot,
|
|
1385
1483
|
workspaceTree,
|
|
1386
1484
|
workspaceFiles: mergedFiles,
|
|
1387
|
-
model: activeModel
|
|
1485
|
+
model: activeModel,
|
|
1486
|
+
signal: abortController.signal
|
|
1388
1487
|
})) {
|
|
1389
1488
|
if (event.type === "run_in_terminal") {
|
|
1390
1489
|
localActionConfirmed = true;
|
|
@@ -1398,30 +1497,37 @@ export const AstraApp = () => {
|
|
|
1398
1497
|
if (event.type === "done") {
|
|
1399
1498
|
break;
|
|
1400
1499
|
}
|
|
1500
|
+
// Flush any accumulated text before the first tool event so that text
|
|
1501
|
+
// appears above the tool cards that follow it — preserving order.
|
|
1502
|
+
if (event.type === "tool_start" || event.type === "run_in_terminal") {
|
|
1503
|
+
commitPending();
|
|
1504
|
+
}
|
|
1401
1505
|
const piece = await handleEvent(event, activeSessionId);
|
|
1402
1506
|
if (piece) {
|
|
1403
|
-
|
|
1404
|
-
|
|
1507
|
+
pendingText += piece;
|
|
1508
|
+
allAssistantText += piece;
|
|
1509
|
+
setStreamingText(normalizeAssistantText(pendingText));
|
|
1405
1510
|
}
|
|
1406
1511
|
}
|
|
1407
1512
|
setStreamingText("");
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
: cleanedAssistant;
|
|
1413
|
-
pushMessage("assistant", guardedAssistant);
|
|
1414
|
-
setChatMessages((prev) => [...prev, { role: "assistant", content: cleanedAssistant }]);
|
|
1513
|
+
commitPending(true);
|
|
1514
|
+
// Update conversation history for the backend with the full combined text.
|
|
1515
|
+
if (allAssistantText.trim()) {
|
|
1516
|
+
setChatMessages((prev) => [...prev, { role: "assistant", content: normalizeAssistantText(allAssistantText) }]);
|
|
1415
1517
|
}
|
|
1416
1518
|
else {
|
|
1417
|
-
setChatMessages((prev) => [...prev, { role: "assistant", content:
|
|
1519
|
+
setChatMessages((prev) => [...prev, { role: "assistant", content: allAssistantText }]);
|
|
1418
1520
|
}
|
|
1419
1521
|
pushMessage("system", "");
|
|
1420
1522
|
}
|
|
1421
1523
|
catch (error) {
|
|
1422
|
-
|
|
1524
|
+
// AbortError fires when user cancels — don't show as an error
|
|
1525
|
+
if (error instanceof Error && error.name !== "AbortError") {
|
|
1526
|
+
pushMessage("error", `Error: ${error.message}`);
|
|
1527
|
+
}
|
|
1423
1528
|
}
|
|
1424
1529
|
finally {
|
|
1530
|
+
abortRunRef.current = null;
|
|
1425
1531
|
setThinking(false);
|
|
1426
1532
|
}
|
|
1427
1533
|
}, [
|
|
@@ -1447,6 +1553,17 @@ export const AstraApp = () => {
|
|
|
1447
1553
|
voiceWaitingForSilence,
|
|
1448
1554
|
workspaceRoot
|
|
1449
1555
|
]);
|
|
1556
|
+
const THINKING_COLORS = ["#6a94f5", "#7aa2ff", "#88b4ff", "#99c4ff", "#aad0ff", "#99c4ff", "#88b4ff", "#7aa2ff"];
|
|
1557
|
+
useEffect(() => {
|
|
1558
|
+
if (!thinking) {
|
|
1559
|
+
setThinkingColorIdx(0);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const interval = setInterval(() => {
|
|
1563
|
+
setThinkingColorIdx((prev) => (prev + 1) % THINKING_COLORS.length);
|
|
1564
|
+
}, 120);
|
|
1565
|
+
return () => clearInterval(interval);
|
|
1566
|
+
}, [thinking]);
|
|
1450
1567
|
useEffect(() => {
|
|
1451
1568
|
if (!voiceQueuedPrompt || !user || thinking) {
|
|
1452
1569
|
return;
|
|
@@ -1509,7 +1626,9 @@ export const AstraApp = () => {
|
|
|
1509
1626
|
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));
|
|
1510
1627
|
}) })), _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 : "--"] })] })] }))] }));
|
|
1511
1628
|
}
|
|
1512
|
-
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})` : ""}
|
|
1629
|
+
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})` : ""}${loopCtx
|
|
1630
|
+
? ` · context: ${loopCtx.in >= 1000 ? `${(loopCtx.in / 1000).toFixed(1)}K` : loopCtx.in} / ${loopCtx.out >= 1000 ? `${(loopCtx.out / 1000).toFixed(1)}K` : loopCtx.out}`
|
|
1631
|
+
: ""}` }), _jsx(Text, { color: "#6c88a8", children: `scope ${workspaceRoot.length > HEADER_PATH_MAX ? `…${workspaceRoot.slice(-(HEADER_PATH_MAX - 1))}` : workspaceRoot} · voice ${voiceEnabled
|
|
1513
1632
|
? voicePreparing
|
|
1514
1633
|
? "on/preparing"
|
|
1515
1634
|
: voiceListening
|
|
@@ -1517,7 +1636,7 @@ export const AstraApp = () => {
|
|
|
1517
1636
|
? "on/waiting"
|
|
1518
1637
|
: "on/listening"
|
|
1519
1638
|
: "on/standby"
|
|
1520
|
-
: "off"}` }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsx(Text, { color: "#3a5068", children: "/help /new /history /voice /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) => {
|
|
1639
|
+
: "off"}` }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsx(Text, { color: "#3a5068", children: "/help /new /history /voice /dictate on|off|status" }), _jsx(Text, { color: "#3a5068", children: isSuperAdmin ? "/tools compact|expanded /settings /logout /exit" : "/settings /logout /exit" }), _jsx(Text, { color: "#2a3a50", children: divider }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [messages.map((message, index) => {
|
|
1521
1640
|
const prev = index > 0 ? messages[index - 1] : null;
|
|
1522
1641
|
const style = styleForKind(message.kind);
|
|
1523
1642
|
const paddedLabel = style.label.padEnd(LABEL_WIDTH, " ");
|
|
@@ -1527,11 +1646,21 @@ export const AstraApp = () => {
|
|
|
1527
1646
|
message.kind !== "system" &&
|
|
1528
1647
|
prev?.kind !== message.kind &&
|
|
1529
1648
|
(message.kind === "assistant" || message.kind === "tool");
|
|
1649
|
+
// Show a subtle turn separator before each assistant response that
|
|
1650
|
+
// follows a tool block — makes it easy to see where one turn ends.
|
|
1651
|
+
const needsTurnSeparator = message.kind === "assistant" &&
|
|
1652
|
+
Boolean(prev) &&
|
|
1653
|
+
prev?.kind === "tool";
|
|
1530
1654
|
if (isSpacing) {
|
|
1531
1655
|
return _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: " " }) }, `${index}-spacer`);
|
|
1532
1656
|
}
|
|
1533
1657
|
if (message.kind === "tool" && message.card && !isSuperAdmin) {
|
|
1534
1658
|
const card = message.card;
|
|
1659
|
+
// In compact mode, hide "start" spinner cards — they create noise
|
|
1660
|
+
// (one per tool call) without adding signal after the run completes.
|
|
1661
|
+
if (toolFeedMode === "compact" && card.kind === "start") {
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1535
1664
|
const localityLabel = card.locality === "LOCAL" ? "local" : "cloud";
|
|
1536
1665
|
const icon = card.kind === "error"
|
|
1537
1666
|
? "✕"
|
|
@@ -1559,11 +1688,16 @@ export const AstraApp = () => {
|
|
|
1559
1688
|
: card.kind === "preview"
|
|
1560
1689
|
? "#9ad5ff"
|
|
1561
1690
|
: "#9bc5ff";
|
|
1562
|
-
|
|
1691
|
+
// Timestamps fade with age: bright for recent, dim for older
|
|
1692
|
+
const tsAge = message.ts ? Date.now() - message.ts : Infinity;
|
|
1693
|
+
const tsColor = tsAge < 120000 ? "#68d5c8" : tsAge < 600000 ? "#3d8a82" : "#2a5a55";
|
|
1694
|
+
return (_jsxs(React.Fragment, { children: [needsTurnSeparator ? _jsx(Text, { color: "#1e2e40", children: "─".repeat(48) }) : needsGroupGap ? _jsx(Text, { children: " " }) : null, _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, children: paddedLabel }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsxs(Text, { color: accent, children: [icon, " ", card.summary, card.path ? _jsxs(Text, { color: "#ffffff", children: [" ", card.path] }) : null, 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] }), message.ts ? _jsxs(Text, { color: tsColor, children: [" ", formatTime(message.ts)] }) : null] })] })] }, `${index}-${message.kind}`));
|
|
1563
1695
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1696
|
+
const showTs = Boolean(message.ts) && (message.kind === "user" || message.kind === "assistant");
|
|
1697
|
+
const tsAge2 = message.ts ? Date.now() - message.ts : Infinity;
|
|
1698
|
+
const tsColor2 = tsAge2 < 120000 ? "#68d5c8" : tsAge2 < 600000 ? "#3d8a82" : "#2a5a55";
|
|
1699
|
+
return (_jsxs(React.Fragment, { children: [needsTurnSeparator ? _jsx(Text, { color: "#1e2e40", children: "─".repeat(48) }) : needsGroupGap ? _jsx(Text, { children: " " }) : null, _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: style.labelColor, bold: style.bold, children: paddedLabel }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [message.kind === "assistant" ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", children: renderMarkdownContent(message.text, style.textColor, `assistant-${index}`) })) : (_jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: style.textColor, bold: style.bold && message.kind === "error", children: message.text }) })), showTs ? _jsxs(Text, { color: tsColor2, children: [" ", formatTime(message.ts)] }) : null] })] })] }, `${index}-${message.kind}`));
|
|
1700
|
+
}), 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 && !streamingText ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#7aa2ff", children: "◆ astra".padEnd(LABEL_WIDTH, " ") }), _jsxs(Text, { color: (THINKING_COLORS[thinkingColorIdx] ?? "#7aa2ff"), children: [_jsx(Spinner, { type: "dots2" }), _jsx(Text, { color: (THINKING_COLORS[thinkingColorIdx] ?? "#7aa2ff"), children: " Working..." }), _jsx(Text, { color: "#2a4060", children: " esc to cancel" })] })] })) : null, voiceEnabled && !thinking ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: "#9ad5ff", children: "🎤 voice".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) => {
|
|
1567
1701
|
void sendPrompt(value);
|
|
1568
1702
|
}, onChange: (value) => {
|
|
1569
1703
|
if (!voiceListening) {
|