@scira/cli 0.1.4 → 0.1.5

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.
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useCallback, useMemo, useRef, useState } from "react";
3
- import { Box, useApp, useStdout, useStdin } from "ink";
4
- import { CHAT_COMMANDS, MENU_VISIBLE } from "./constants.js";
3
+ import { Box, Text, useApp, useStdout, useStdin } from "ink";
4
+ import { CHAT_COMMANDS, MENU_VISIBLE, SPINNER_FRAMES, LOADING_PHRASES } from "./constants.js";
5
5
  import { CWD_DISPLAY, wrapText, wrapInputWithCursor, loadInputHistory, saveInputHistory, linkAtMouseColumn, openExternalUrl } from "./lib/utils.js";
6
6
  import { deleteRun } from "../../storage/run-store.js";
7
7
  import { saveGlobalConfig } from "../../config/load-config.js";
@@ -346,12 +346,25 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
346
346
  const menuHeight = commandMenuHeight + helpHeight + approvalHeight + linkHeight;
347
347
  const feedRows = Math.max(3, rows - 6 - inputLines.length - menuHeight);
348
348
  const hasRunningTool = feed.some((it) => it.kind === "tool" && it.status === "running");
349
- const { lines: feedLines, toggleAtLine, groupToggleAtLine, linkAtLine } = useFeedLines(feed, innerWidth, reasoningTick, hasRunningTool ? frame : 0, collapsedGroups, focusedGroupKey, itemExpandState, hoveredIdx, config);
349
+ const { lines: feedLines, toggleAtLine, groupToggleAtLine, linkAtLine, lastUserLineStart } = useFeedLines(feed, innerWidth, reasoningTick, hasRunningTool ? frame : 0, collapsedGroups, focusedGroupKey, itemExpandState, hoveredIdx, config);
350
350
  const contentRows = Math.max(1, feedRows);
351
351
  const maxScrollOffset = Math.max(0, feedLines.length - contentRows);
352
352
  wheelStateRef.current = { screen, maxScrollOffset };
353
- const clampedOffset = Math.min(scrollOffset, maxScrollOffset);
354
- const startIdx = Math.max(0, feedLines.length - contentRows - clampedOffset);
353
+ // scrollOffset < 0 is a sentinel meaning "pin the most recent user message to
354
+ // the top of the viewport" (with empty space below for the incoming reply).
355
+ // Once the reply grows past the viewport we fall back to bottom-anchoring so
356
+ // the streaming output stays visible. Any manual scroll clears the sentinel.
357
+ const pinUserToTop = scrollOffset < 0 && lastUserLineStart >= 0;
358
+ let startIdx;
359
+ if (pinUserToTop) {
360
+ const fitsBelow = feedLines.length - lastUserLineStart <= contentRows;
361
+ startIdx = fitsBelow ? lastUserLineStart : Math.max(0, feedLines.length - contentRows);
362
+ }
363
+ else {
364
+ const off = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
365
+ startIdx = Math.max(0, feedLines.length - contentRows - off);
366
+ }
367
+ const clampedOffset = Math.max(0, feedLines.length - contentRows - startIdx);
355
368
  const hasLinkHover = hoveredIdx !== null && (linkAtLine.get(hoveredIdx)?.length ?? 0) > 0;
356
369
  const feedStartRow = 3;
357
370
  if (screen === "chat") {
@@ -384,7 +397,31 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
384
397
  clickMapRef.current = clickMap;
385
398
  hoverMapRef.current = hoverMap;
386
399
  }
387
- const visibleLines = feedLines.slice(startIdx, startIdx + contentRows);
400
+ const slicedLines = feedLines.slice(startIdx, startIdx + contentRows);
401
+ // The feed box is bottom-aligned (justifyContent="flex-end"). When pinning the
402
+ // user message to the top, pad blank lines below it so short content is pushed
403
+ // up — the user message sits at the top with empty room below for the reply.
404
+ const blankLine = (k) => _jsx(Text, { children: " " }, k);
405
+ // Show an animated loading line whenever the agent is still doing non-text
406
+ // work (reasoning, tool calls, status) — i.e. the latest feed item isn't the
407
+ // streamed answer text. It stays pinned below the latest content for the whole
408
+ // turn, and the phrase rotates slowly so it doesn't feel frozen. The feed box
409
+ // is bottom-aligned with overflow hidden, so when the timeline is long the
410
+ // loader stays visible and the oldest lines scroll off the top instead.
411
+ const lastItem = feed[feed.length - 1];
412
+ const showLoader = busy && lastItem !== undefined && lastItem.kind !== "text";
413
+ const phrase = LOADING_PHRASES[Math.floor(frame / 24) % LOADING_PHRASES.length];
414
+ const loadingLine = (_jsx(Text, { dimColor: true, children: `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${phrase}` }, "loading"));
415
+ const contentWithLoader = showLoader
416
+ ? (slicedLines.length > 0 ? [...slicedLines, blankLine("loading-gap"), loadingLine] : [loadingLine])
417
+ : slicedLines;
418
+ const visibleLines = pinUserToTop && contentWithLoader.length < contentRows
419
+ ? [
420
+ blankLine("pad-top"),
421
+ ...contentWithLoader,
422
+ ...Array.from({ length: Math.max(0, contentRows - contentWithLoader.length - 1) }, (_, i) => blankLine(`pad-${i}`)),
423
+ ]
424
+ : contentWithLoader;
388
425
  const scrollLabel = clampedOffset > 0
389
426
  ? (startIdx > 0 ? `↑ ${startIdx} · ↓ ${clampedOffset} · wheel/⇞⇟` : `top · ↓ ${clampedOffset} · wheel/⇞⇟`)
390
427
  : "";
@@ -51,6 +51,33 @@ export const TOOL_ICONS = {
51
51
  grepWorkspace: "⌕"
52
52
  };
53
53
  export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
54
+ export const LOADING_PHRASES = [
55
+ "Thinking it through…",
56
+ "Digging into it…",
57
+ "Connecting the dots…",
58
+ "Gathering context…",
59
+ "Working through it…",
60
+ "Sifting the details…",
61
+ "Putting it together…",
62
+ "Chasing down answers…",
63
+ "Mulling it over…",
64
+ "Lining things up…",
65
+ "Reading the room…",
66
+ "Scanning the sources…",
67
+ "Cross-checking facts…",
68
+ "Tracing the threads…",
69
+ "Weighing the options…",
70
+ "Following the trail…",
71
+ "Piecing it together…",
72
+ "Untangling the details…",
73
+ "Skimming the fine print…",
74
+ "Joining the dots…",
75
+ "Hunting for specifics…",
76
+ "Sorting signal from noise…",
77
+ "Drafting the answer…",
78
+ "Double-checking the work…",
79
+ "Wrapping my head around it…",
80
+ ];
54
81
  export const HOME_TIPS = [
55
82
  "Type a question and press ⏎ to start a new research run.",
56
83
  "Say \"deep research …\" or \"compare …\" to trigger the full research harness.",
@@ -26,7 +26,9 @@ export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullMode
26
26
  const controller = new AbortController();
27
27
  session.abort = controller;
28
28
  setBusy(true);
29
- setScrollOffset(0);
29
+ // Pin the just-sent user message to the top of the viewport, leaving room
30
+ // below for the incoming assistant reply (-1 sentinel; see SciraApp).
31
+ setScrollOffset(-1);
30
32
  sessionSetBusy(runPath, true);
31
33
  const modelId = config.model;
32
34
  const turnStartedAt = Date.now();
@@ -63,6 +63,7 @@ reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState,
63
63
  const toggleAtLine = new Map();
64
64
  const groupToggleAtLine = new Map();
65
65
  const linkAtLine = new Map();
66
+ let lastUserLineStart = -1;
66
67
  let key = 0;
67
68
  const { groupOf, groups } = computeGroups(feed);
68
69
  const eff = [];
@@ -188,6 +189,7 @@ reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState,
188
189
  const rightPad = time ? time.length + 1 : 0;
189
190
  const wrapped = wrapText(fi.text, Math.max(10, bandW - 4 - rightPad));
190
191
  const blank = " ".repeat(bandW);
192
+ lastUserLineStart = lines.length;
191
193
  lines.push(_jsx(Text, { ...bandBg, children: blank }, key++));
192
194
  wrapped.forEach((l, idx) => {
193
195
  const isFirst = idx === 0;
@@ -233,7 +235,7 @@ reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState,
233
235
  }
234
236
  }
235
237
  });
236
- return { lines, toggleAtLine, groupToggleAtLine, linkAtLine };
238
+ return { lines, toggleAtLine, groupToggleAtLine, linkAtLine, lastUserLineStart };
237
239
  // eslint-disable-next-line react-hooks/exhaustive-deps
238
240
  }, [feed, innerWidth, reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState, hoveredLineIdx, config, theme]);
239
241
  }
@@ -20,6 +20,10 @@ export function isCollapsibleToolName(name) {
20
20
  return name.length > 0;
21
21
  }
22
22
  export function defaultCollapsedToolName(name) {
23
+ // Chrome DevTools MCP tools (prefixed `devtools_`) produce long browser
24
+ // snapshots/output, so collapse them by default like the built-in tools.
25
+ if (name.startsWith("devtools_"))
26
+ return true;
23
27
  return DEFAULT_COLLAPSED_TOOLS.has(name);
24
28
  }
25
29
  export function isToolItemCollapsed(id, name, status, expandState) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scira/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Scira — terminal-native AI research agent with grounded sources, verified claims, and local run storage.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,14 +39,14 @@
39
39
  "test:watch": "vitest"
40
40
  },
41
41
  "dependencies": {
42
- "@ai-sdk/mcp": "^1.0.48",
43
- "@ai-sdk/openai-compatible": "^2.0.49",
44
- "@ai-sdk/xai": "^3.0.94",
42
+ "@ai-sdk/mcp": "^1.0.49",
43
+ "@ai-sdk/openai-compatible": "^2.0.50",
44
+ "@ai-sdk/xai": "^3.0.95",
45
45
  "@clack/prompts": "^1.5.1",
46
46
  "@mendable/firecrawl-js": "^4.25.3",
47
47
  "@modelcontextprotocol/sdk": "^1.29.0",
48
48
  "@mozilla/readability": "^0.6.0",
49
- "ai": "^6.0.202",
49
+ "ai": "^6.0.203",
50
50
  "diff": "^9.0.0",
51
51
  "exa-js": "^2.13.0",
52
52
  "files-sdk": "^1.8.0",