@athenaintel/react 0.40.0 → 0.40.1

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
@@ -561,7 +561,25 @@ the loaded unread rows; **Dismiss** uses
561
561
  session — a dismissed session comes back with its next message).
562
562
  `InboxButton` is a bell with the unread count (`useAthenaInboxUnreadCount`,
563
563
  the Unread tab's first page: `50+` past one page) that opens the inbox in a
564
- popover.
564
+ popover. Each tab shows its count once its list has answered, and nothing
565
+ before.
566
+
567
+ ### What counts as read
568
+
569
+ `AthenaChat` marks the session it shows read for the viewer, as Athena's own
570
+ chat does, wherever the session was opened from (`SessionList`, the inbox, a
571
+ `?session_id=` link): once the chat is in front of the person (its window,
572
+ a pop-out's included, visible and focused, and the chat itself on screen, not
573
+ hidden behind a maximized workbench), the session's history is on screen, no
574
+ run is going and its row is unread, for a second.
575
+ The write is the provider's `setReadState` with the row's `updatedAt`, so
576
+ every list under the provider flips at once and the next message shows the
577
+ session unread again. A message that arrives while the person watches is
578
+ marked too (a running session is marked when its run ends, never per
579
+ streamed chunk); a read session is never written; and a session the person
580
+ marks unread from a list stays unread until something new arrives or they
581
+ open it again. A host that draws its own transcript instead of `AthenaChat`
582
+ calls `setReadState` itself.
565
583
 
566
584
  ### Server filters and older servers
567
585
 
@@ -789,6 +807,16 @@ Both use the same bearer / API key as session creation. The full recipe
789
807
  template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
790
808
  exported if you hold the bridge state yourself.
791
809
 
810
+ ## What's new in 0.40.1
811
+
812
+ **The session you are looking at is marked read.** In `enableThreadList` mode, `AthenaChat` now writes the viewer's read receipt for the session it shows, as Athena's own chat does: once the chat is in front of the person (its window, a pop-out's included, visible and focused, and the chat itself on screen, not hidden behind a maximized workbench), the session's history is on screen, no run is going and its row is unread, for a second. A session opened from `SessionList`, the inbox or a `?session_id=` link used to stay unread for good (only the inbox's own open, its Mark all read and the sessions page toggle wrote the receipt), so unread dots and the inbox's Unread count only grew. The write is the provider's `setReadState` (`POST /api/conversations/threads/read-state`, the route the inbox already used) with the row's `updatedAt`, so every list flips at once and the next message shows the session unread again. A message that arrives while the person watches is marked too, with at most one write per message; nothing polls, a read session is never written, and a session the person marks unread stays unread until something new arrives or they open it again. See [What counts as read](#what-counts-as-read).
813
+
814
+ **The inbox shows a tab's count once it has one.** The All and Unread tabs counted their rows before their first page answered ("All 0 · Unread 14" for a moment). A tab now shows no count until its list has answered.
815
+
816
+ **A running tool is never called stalled.** While the agent runs a long command, a build or a test suite (or a tool such as Python execution that shows only its streaming placeholder while it works), the turn header no longer adds "Nothing new for 1m 00s" in amber, and the line under an ungrouped turn keeps the tool's verb with a neutral counter ("Running a command… · 1m 04s") instead of "Still working — nothing new". The stall tone stays for a quiet stream that waits on the model.
817
+
818
+ Nothing to change in your app.
819
+
792
820
  ## What's new in 0.40.0
793
821
 
794
822
  **`AthenaChat` shows an agent's UI test recording.** When an agent with the UI Testing toolkit checks a change in its computer's browser, `test_recording_stop` now renders a report card: the recording's title, "N passed · M failed" (with "· K untested" when a test was never checked), each test with its ✓ / ✗ assertions and what the agent saw (a failed assertion is tinted red, a test with no assertion reads "Not checked"), the summary, and the edited video. Every test's start time is a chapter: click it and the video plays from there (an assertion's time works the same way). A **Watch in Athena** link opens the asset in Athena. The card stays in the conversation instead of folding into the turn's collapsed steps. `test_recording_start` and `test_recording_mark` each render one compact row ("Recording started", "Test", "Pass", "Fail", "Note" with the time in the recording), and their collapsed steps read "Started recording "…"", "Started test: It should …", "Passed: …", "Failed: …".
@@ -1934,7 +1962,7 @@ Your own portals can read the same context: `usePortalContainer()` returns the p
1934
1962
 
1935
1963
  - **The agent's sentences stay visible.** Every text part the agent writes before its last tool call ("I'll find your workstation, then check origin/staging.", "Started. Now I'll compare.") renders in the transcript, in order, as it streams. Only the work between two sentences folds: each run of tool calls and reasoning becomes one collapsed step row. The answer (the text after the last tool call) renders below, as before; no text renders twice.
1936
1964
  - **Each step says what it did.** A collapsed row names the step in progress, else the last one it took — "Ran `git log -1 origin/staging`", "Read `live_audio.py`", "Edited `upstream.py`" with "+4 −1", "Found 3 assets for "workstation"", "jevin-box is running", "Viewed pull request #43093", "Updated plan (3/7 done)" — with "+2 more" when the row folds several. Opening it lists every step, and "Show tool details" reveals that row's tool UIs.
1937
- - **The header counts the turn.** "Working for 1m 12s" while it runs, "Worked for 2m 30s · Used 7 tools" once done (from the messages' own server stamps), "Waiting for you" on an approval. After thirty seconds without a streamed change the header adds "Nothing new for 42s" in amber.
1965
+ - **The header counts the turn.** "Working for 1m 12s" while it runs, "Worked for 2m 30s · Used 7 tools" once done (from the messages' own server stamps), "Waiting for you" on an approval. After thirty seconds in which the model has streamed nothing the header adds "Nothing new for 42s" in amber — but never while a tool call is still running: a command, a build or a test run streams nothing for minutes while it works, so its step row says it is running and the header goes on counting the turn. With `groupToolCalls={false}` the line under the turn keeps the running tool's verb and a neutral counter ("Running a command… · 1m 04s") the same way.
1938
1966
 
1939
1967
  The transcript is `SuperGroupingTranscript` from `@athenaintel/react/super-grouping`; it takes the same renderer and host slots as `SuperGroupingCard` (which keeps the single-card layout for hosts that mount it). `splitTurnSegments`, `readTurnTiming` and `formatWorkDuration` are exported for hosts that lay a turn out themselves.
1940
1968
 
@@ -49,6 +49,7 @@ const lucideReact = require("lucide-react");
49
49
  const react$1 = require("react");
50
50
  const threadIdContext = require("../threads/thread-id-context.cjs");
51
51
  const statewireThreadList = require("../threads/statewire-thread-list.cjs");
52
+ const markViewedThreadRead = require("../threads/mark-viewed-thread-read.cjs");
52
53
  const FileUploadButton = require("../composer/FileUploadButton.cjs");
53
54
  const ComposerQuotePreview = require("../composer/ComposerQuotePreview.cjs");
54
55
  const ComposerAttachmentPreview = require("../composer/ComposerAttachmentPreview.cjs");
@@ -252,7 +253,10 @@ const AthenaChat = ({
252
253
  return /* @__PURE__ */ jsxRuntime.jsx(AthenaChatDefaultComponentsContext.Provider, { value: defaultComponentsContextValue, children: /* @__PURE__ */ jsxRuntime.jsx(AthenaChatErrorBoundary.AthenaChatErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx(toolFallback.ToolFallbackAppearanceProvider, { appearance: "thread", children: /* @__PURE__ */ jsxRuntime.jsx(apiClient.MentionAuthCtx.Provider, { value: mentionAuth, children: /* @__PURE__ */ jsxRuntime.jsx(
253
254
  chatThreadShell.ChatThreadShell,
254
255
  {
255
- overlay: /* @__PURE__ */ jsxRuntime.jsx(threadLoadingOverlay.ThreadLoadingOverlay, {}),
256
+ overlay: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
257
+ /* @__PURE__ */ jsxRuntime.jsx(threadLoadingOverlay.ThreadLoadingOverlay, {}),
258
+ /* @__PURE__ */ jsxRuntime.jsx(markViewedThreadRead.MarkViewedThreadRead, {})
259
+ ] }),
256
260
  panelClassName: cnfast.cn(
257
261
  "aui-root aui-thread-root @container relative flex h-full flex-col bg-background",
258
262
  className
@@ -598,21 +602,22 @@ const MessageError = () => /* @__PURE__ */ jsxRuntime.jsx(react.MessagePrimitive
598
602
  }
599
603
  ) })
600
604
  ] }) });
601
- function useSecondsSince(resetKey) {
605
+ function useSecondsSince({ resetKey, ticking = true }) {
602
606
  const [seconds, setSeconds] = react$1.useState(0);
603
607
  react$1.useEffect(() => {
604
608
  setSeconds(0);
609
+ if (!ticking) return;
605
610
  const startedAt = Date.now();
606
611
  const timer = setInterval(() => {
607
612
  setSeconds(Math.floor((Date.now() - startedAt) / 1e3));
608
613
  }, 1e3);
609
614
  return () => clearInterval(timer);
610
- }, [resetKey]);
615
+ }, [resetKey, ticking]);
611
616
  return seconds;
612
617
  }
613
- const RunStatusLine = ({ verb, resetKey, className }) => {
614
- const seconds = useSecondsSince(resetKey);
615
- const stalled = seconds >= runActivity.RUN_ACTIVITY_STALL_SECONDS;
618
+ const RunStatusLine = ({ verb, resetKey, toolCallInFlight = false, className }) => {
619
+ const seconds = useSecondsSince({ resetKey });
620
+ const stalled = runActivity.isRunActivityStalled({ seconds, toolCallInFlight });
616
621
  return /* @__PURE__ */ jsxRuntime.jsxs(
617
622
  "div",
618
623
  {
@@ -638,7 +643,7 @@ const RunStatusLine = ({ verb, resetKey, className }) => {
638
643
  {
639
644
  "aria-hidden": "true",
640
645
  className: cnfast.cn("tabular-nums", stalled && "text-amber-600 dark:text-amber-400"),
641
- children: runActivity.runActivityLabel(seconds, verb)
646
+ children: runActivity.runActivityLabel({ seconds, verb, stalled })
642
647
  }
643
648
  ),
644
649
  /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-live": "polite", className: "sr-only", children: stalled ? "Assistant is still working; nothing new for over thirty seconds." : `${verb}…` })
@@ -668,13 +673,22 @@ const AthenaMessageActivityIndicator = () => {
668
673
  });
669
674
  const activityKey = react.useAuiState((s) => runActivity.runActivityKey(s.message.parts));
670
675
  const verb = react.useAuiState((s) => runActivity.runActivityVerb(s.message.parts));
676
+ const toolCallInFlight = react.useAuiState((s) => runActivity.isToolCallInFlight(s.message.parts));
671
677
  if (!isRunning || activityKey === null) return null;
672
- return /* @__PURE__ */ jsxRuntime.jsx(RunStatusLine, { verb: verb ?? "Working", resetKey: activityKey });
678
+ return /* @__PURE__ */ jsxRuntime.jsx(
679
+ RunStatusLine,
680
+ {
681
+ verb: verb ?? "Working",
682
+ resetKey: activityKey,
683
+ toolCallInFlight
684
+ }
685
+ );
673
686
  };
674
687
  const AthenaGroupedRunMeta = () => {
675
688
  const activityKey = react.useAuiState((s) => runActivity.runActivityKey(s.message.parts));
676
- const seconds = useSecondsSince(activityKey ?? "thinking");
677
- const stalled = seconds >= runActivity.RUN_ACTIVITY_STALL_SECONDS;
689
+ const toolCallInFlight = react.useAuiState((s) => runActivity.isToolCallInFlight(s.message.parts));
690
+ const seconds = useSecondsSince({ resetKey: activityKey ?? "thinking", ticking: !toolCallInFlight });
691
+ const stalled = runActivity.isRunActivityStalled({ seconds, toolCallInFlight });
678
692
  if (!stalled) return null;
679
693
  return /* @__PURE__ */ jsxRuntime.jsxs(
680
694
  "span",