@invergent/agent-chat-react 2.7.0 → 2.7.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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cn, __publicField } from './chunk-47MF6JUW.js';
2
2
  import { createContext, memo, useRef, useEffect, useCallback, useMemo, useState, useLayoutEffect, lazy, useContext, Fragment as Fragment$1, Children, Suspense } from 'react';
3
- import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, File as File$1, FileImage, FileAudio, FileVideo, FileText, FileCode, FileArchive, FileSpreadsheet, PlugZapIcon, ChevronRightIcon, ArrowLeftIcon, Loader2Icon, InboxIcon, Loader2, AlertCircle, Pause, Play, Ban, TargetIcon, CalendarClockIcon, UsersIcon, MessageSquareIcon, ZapIcon, Maximize2Icon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ExternalLinkIcon, ClipboardCheckIcon, ShieldAlertIcon, TimerIcon, CircleDotIcon, XIcon, Trash2Icon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, CheckIcon, Clock, ArrowRight, TerminalIcon, SparklesIcon, ClockIcon, PlusIcon, CloudIcon, HardDriveIcon, PaperclipIcon, GlobeIcon, FolderIcon, CopyIcon, FileIcon, ChevronLeftIcon, MinusIcon, SearchIcon, CircleDot, XCircle, CheckCircle2, ArrowUpIcon, CircleCheckIcon, ThumbsUpIcon, ThumbsDownIcon, BookOpenIcon, GitBranchIcon, ActivityIcon, EyeIcon, ListTodoIcon, BotIcon, ListIcon, PenLineIcon, FileEditIcon, WrenchIcon, FileCodeIcon, CodeIcon, FileArchiveIcon, FileVideoIcon, FileAudioIcon, FileImageIcon, FileChartLineIcon, FileSpreadsheetIcon, XCircleIcon, CheckCircle2Icon, Code, CircleXIcon, SkullIcon, CheckCircleIcon, CircleIcon } from 'lucide-react';
3
+ import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, CreditCard, File as File$1, FileImage, FileAudio, FileVideo, FileText, FileCode, FileArchive, FileSpreadsheet, PlugZapIcon, ChevronRightIcon, ArrowLeftIcon, Loader2Icon, InboxIcon, Loader2, AlertCircle, Pause, Play, Ban, TargetIcon, CalendarClockIcon, UsersIcon, MessageSquareIcon, ZapIcon, Maximize2Icon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ExternalLinkIcon, ClipboardCheckIcon, ShieldAlertIcon, TimerIcon, CircleDotIcon, XIcon, Trash2Icon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, CheckIcon, Clock, ArrowRight, TerminalIcon, SparklesIcon, ClockIcon, PlusIcon, CloudIcon, HardDriveIcon, PaperclipIcon, GlobeIcon, FolderIcon, CopyIcon, FileIcon, ChevronLeftIcon, MinusIcon, SearchIcon, CircleDot, XCircle, CheckCircle2, ArrowUpIcon, CircleCheckIcon, ThumbsUpIcon, ThumbsDownIcon, BookOpenIcon, GitBranchIcon, ActivityIcon, EyeIcon, ListTodoIcon, BotIcon, ListIcon, PenLineIcon, FileEditIcon, WrenchIcon, FileCodeIcon, CodeIcon, FileArchiveIcon, FileVideoIcon, FileAudioIcon, FileImageIcon, FileChartLineIcon, FileSpreadsheetIcon, XCircleIcon, CheckCircle2Icon, Code, CircleXIcon, SkullIcon, CheckCircleIcon, CircleIcon } from 'lucide-react';
4
4
  import { cva } from 'class-variance-authority';
5
5
  import { Collapsible as Collapsible$1, Slot, Tooltip as Tooltip$1, Progress as Progress$1, Tabs as Tabs$1, Dialog as Dialog$1, ScrollArea as ScrollArea$1, Popover as Popover$1 } from 'radix-ui';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -4663,6 +4663,74 @@ function AttachmentChip({
4663
4663
  }
4664
4664
  );
4665
4665
  }
4666
+ function tokenizeInline(text) {
4667
+ const tokens = [];
4668
+ let buf = "";
4669
+ let i = 0;
4670
+ const flush = () => {
4671
+ if (buf) {
4672
+ tokens.push({ kind: "text", value: buf });
4673
+ buf = "";
4674
+ }
4675
+ };
4676
+ while (i < text.length) {
4677
+ const ch = text[i];
4678
+ if (ch === "`") {
4679
+ const end = text.indexOf("`", i + 1);
4680
+ if (end > i) {
4681
+ flush();
4682
+ tokens.push({ kind: "code", value: text.slice(i + 1, end) });
4683
+ i = end + 1;
4684
+ continue;
4685
+ }
4686
+ }
4687
+ if (ch === "*" && text[i + 1] === "*") {
4688
+ const end = text.indexOf("**", i + 2);
4689
+ if (end > i + 1) {
4690
+ flush();
4691
+ tokens.push({ kind: "strong", value: text.slice(i + 2, end) });
4692
+ i = end + 2;
4693
+ continue;
4694
+ }
4695
+ }
4696
+ if (ch === "*") {
4697
+ const end = text.indexOf("*", i + 1);
4698
+ if (end > i + 1) {
4699
+ flush();
4700
+ tokens.push({ kind: "em", value: text.slice(i + 1, end) });
4701
+ i = end + 1;
4702
+ continue;
4703
+ }
4704
+ }
4705
+ buf += ch;
4706
+ i += 1;
4707
+ }
4708
+ flush();
4709
+ return tokens;
4710
+ }
4711
+ function renderInlineMarkdown(text) {
4712
+ if (!text || !/[*`]/.test(text)) return text ?? null;
4713
+ const tokens = tokenizeInline(text);
4714
+ return tokens.map((tok, idx) => {
4715
+ switch (tok.kind) {
4716
+ case "strong":
4717
+ return /* @__PURE__ */ jsx("strong", { className: "font-semibold", children: tok.value }, idx);
4718
+ case "em":
4719
+ return /* @__PURE__ */ jsx("em", { children: tok.value }, idx);
4720
+ case "code":
4721
+ return /* @__PURE__ */ jsx(
4722
+ "code",
4723
+ {
4724
+ className: "rounded bg-muted px-1 py-0.5 font-mono text-[0.9em]",
4725
+ children: tok.value
4726
+ },
4727
+ idx
4728
+ );
4729
+ default:
4730
+ return /* @__PURE__ */ jsx(Fragment$1, { children: tok.value }, idx);
4731
+ }
4732
+ });
4733
+ }
4666
4734
  function InputGroup({ className, ...props }) {
4667
4735
  return /* @__PURE__ */ jsx(
4668
4736
  "div",
@@ -6066,6 +6134,7 @@ function ChatComposerInner({
6066
6134
  canShowBrowser = false,
6067
6135
  canShowWorkspace = false,
6068
6136
  deepResearchEnabled = false,
6137
+ researchEnabled = false,
6069
6138
  codeAgentsEnabled = false,
6070
6139
  viewMode = "simple",
6071
6140
  onViewModeChange
@@ -6136,6 +6205,13 @@ function ChatComposerInner({
6136
6205
  description: "Delegate a topic to the deep-research workflow"
6137
6206
  });
6138
6207
  }
6208
+ if (researchEnabled) {
6209
+ base.push({
6210
+ value: "/auto-research ",
6211
+ label: "/auto-research",
6212
+ description: "Launch an autonomous research mission (Arbor)"
6213
+ });
6214
+ }
6139
6215
  if (codeAgentsEnabled) {
6140
6216
  base.push(
6141
6217
  { value: "/code claude ", label: "/code claude", description: "Run Claude Code on the workspace (your plan)" },
@@ -6147,7 +6223,7 @@ function ChatComposerInner({
6147
6223
  }
6148
6224
  return base;
6149
6225
  },
6150
- [deepResearchEnabled, codeAgentsEnabled]
6226
+ [deepResearchEnabled, researchEnabled, codeAgentsEnabled]
6151
6227
  );
6152
6228
  const scheduledExamples = useMemo(
6153
6229
  () => [
@@ -7668,6 +7744,16 @@ var ErrorMessage = memo(function ErrorMessage2({
7668
7744
  const [detailOpen, setDetailOpen] = useState(false);
7669
7745
  const [retryPending, setRetryPending] = useState(false);
7670
7746
  const [retryError, setRetryError] = useState(null);
7747
+ if (errorInfo.insufficientCredits) {
7748
+ return /* @__PURE__ */ jsx(
7749
+ InsufficientCreditsCard,
7750
+ {
7751
+ detail: errorInfo.detail,
7752
+ onDismiss,
7753
+ className
7754
+ }
7755
+ );
7756
+ }
7671
7757
  const showRetry = errorInfo.retryable && !!onRetry;
7672
7758
  const handleRetry = async () => {
7673
7759
  if (!onRetry) return;
@@ -7739,6 +7825,46 @@ var ErrorMessage = memo(function ErrorMessage2({
7739
7825
  }
7740
7826
  );
7741
7827
  });
7828
+ function parseRequestedTokens(detail) {
7829
+ const m = detail.match(/["']?requested["']?\s*[:=]\s*(\d+)/);
7830
+ return m ? Number(m[1]) : null;
7831
+ }
7832
+ var InsufficientCreditsCard = memo(function InsufficientCreditsCard2({
7833
+ detail,
7834
+ onDismiss,
7835
+ className
7836
+ }) {
7837
+ const { onOpenBilling } = useAgentChatAdapterContext();
7838
+ const requested = parseRequestedTokens(detail);
7839
+ return /* @__PURE__ */ jsxs(
7840
+ "div",
7841
+ {
7842
+ role: "alert",
7843
+ className: cn(
7844
+ "flex w-full gap-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-4",
7845
+ className
7846
+ ),
7847
+ children: [
7848
+ /* @__PURE__ */ jsx(CreditCard, { className: "mt-0.5 size-5 shrink-0 text-amber-600 dark:text-amber-400" }),
7849
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [
7850
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-foreground", children: "You're out of credits" }),
7851
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
7852
+ "This run was blocked by the token-credit limit",
7853
+ requested ? ` (it needed ~${requested.toLocaleString()} tokens)` : "",
7854
+ ". Buy additional credits or upgrade your plan to keep your agents running."
7855
+ ] }),
7856
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2 pt-1", children: [
7857
+ onOpenBilling && /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: onOpenBilling, children: [
7858
+ /* @__PURE__ */ jsx(CreditCard, { className: "size-4" }),
7859
+ "Go to Billing"
7860
+ ] }),
7861
+ onDismiss && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: onDismiss, children: "Dismiss" })
7862
+ ] })
7863
+ ] })
7864
+ ]
7865
+ }
7866
+ );
7867
+ });
7742
7868
  var SKELETON_WIDTHS = [70, 85, 55, 90, 60, 78, 45, 82, 65, 72];
7743
7869
  var PDF_WORKER_SRC = new URL(
7744
7870
  "pdfjs-dist/legacy/build/pdf.worker.mjs",
@@ -8756,7 +8882,11 @@ function cancelledToolLabel(toolName) {
8756
8882
  research_memory: "Research",
8757
8883
  research_outline: "Research",
8758
8884
  generate_image: "image generation",
8759
- generate_video: "video generation"
8885
+ generate_video: "video generation",
8886
+ // Arbor research-mission tools (the /auto-research coordinator loop).
8887
+ idea_tree: "Idea tree",
8888
+ dispatch_experiments: "Dispatch experiments",
8889
+ merge_experiment: "Merge experiment"
8760
8890
  };
8761
8891
  if (map[toolName]) return map[toolName];
8762
8892
  if (toolName.startsWith("mcp__")) return formatMcpToolLabel(toolName);
@@ -8933,7 +9063,7 @@ function NarrationEntry({
8933
9063
  /* @__PURE__ */ jsx(TimelineSeparator, { style: { backgroundColor: "var(--color-border)" } }),
8934
9064
  /* @__PURE__ */ jsx(TimelineIndicator, { className: "size-2 border-none bg-foreground/30" })
8935
9065
  ] }),
8936
- /* @__PURE__ */ jsx(TimelineContent, { children: /* @__PURE__ */ jsx("p", { children: line }) })
9066
+ /* @__PURE__ */ jsx(TimelineContent, { children: /* @__PURE__ */ jsx("p", { children: renderInlineMarkdown(line) }) })
8937
9067
  ] });
8938
9068
  }
8939
9069
  function TextEntry({
@@ -9049,6 +9179,17 @@ function extractToolDetail(tc) {
9049
9179
  const prompt = stringArg("prompt");
9050
9180
  return prompt ? truncate3(prompt, 60) : null;
9051
9181
  }
9182
+ case "idea_tree":
9183
+ return stringArg("action");
9184
+ case "dispatch_experiments": {
9185
+ const keys = args.node_keys;
9186
+ if (Array.isArray(keys) && keys.length > 0) {
9187
+ return truncate3(keys.map((k) => String(k)).join(", "), 40);
9188
+ }
9189
+ return stringArg("action");
9190
+ }
9191
+ case "merge_experiment":
9192
+ return stringArg("node_key") ?? stringArg("action");
9052
9193
  case "memory":
9053
9194
  return stringArg("action") ?? stringArg("key");
9054
9195
  case "research_memory": {
@@ -9350,7 +9491,7 @@ function IterationGroup({
9350
9491
  "aria-expanded": open,
9351
9492
  className: "flex w-full items-center gap-2 rounded py-0.5 text-left text-sm hover:bg-muted/40",
9352
9493
  children: [
9353
- /* @__PURE__ */ jsx("span", { className: cn("flex-1 truncate", labelTone), children: label }),
9494
+ /* @__PURE__ */ jsx("span", { className: cn("flex-1 truncate", labelTone), children: renderInlineMarkdown(label) }),
9354
9495
  /* @__PURE__ */ jsx(
9355
9496
  ChevronDown,
9356
9497
  {
@@ -9477,7 +9618,7 @@ function SimpleAssistantGroup({
9477
9618
  const narrationLine = action ? action.body.trim().toLowerCase() === "done" ? null : action.body : inferredNarration;
9478
9619
  const askBody = hasUserFacingAskContent(message) ? cleaned : null;
9479
9620
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
9480
- askBody ? /* @__PURE__ */ jsx(MessageResponse, { children: askBody }) : narrationLine ? /* @__PURE__ */ jsx("p", { className: "text-sm text-foreground", children: narrationLine }) : null,
9621
+ askBody ? /* @__PURE__ */ jsx(MessageResponse, { children: askBody }) : narrationLine ? /* @__PURE__ */ jsx("p", { className: "text-sm text-foreground", children: renderInlineMarkdown(narrationLine) }) : null,
9481
9622
  /* @__PURE__ */ jsx(
9482
9623
  IterationGroup,
9483
9624
  {
@@ -9639,6 +9780,7 @@ function ChatThread({
9639
9780
  viewMode = "simple",
9640
9781
  onViewModeChange,
9641
9782
  deepResearchEnabled = false,
9783
+ researchEnabled = false,
9642
9784
  codeAgentsEnabled = false,
9643
9785
  researchSources = [],
9644
9786
  hideTurnSummary = false,
@@ -9705,6 +9847,7 @@ function ChatThread({
9705
9847
  viewMode,
9706
9848
  onViewModeChange,
9707
9849
  deepResearchEnabled,
9850
+ researchEnabled,
9708
9851
  codeAgentsEnabled
9709
9852
  }
9710
9853
  ),
@@ -10539,6 +10682,9 @@ var AGENT_CHAT_LISTENED_EVENTS = [
10539
10682
  "iteration.summary",
10540
10683
  "turn.summary"
10541
10684
  ];
10685
+ var AGENT_CHAT_LISTENED_EVENT_SET = new Set(
10686
+ AGENT_CHAT_LISTENED_EVENTS
10687
+ );
10542
10688
 
10543
10689
  // src/runtime/reducer.ts
10544
10690
  var EMPTY_TOKEN_USAGE = {
@@ -10561,6 +10707,7 @@ function createInitialAgentChatState(options = {}) {
10561
10707
  sessionDone: false,
10562
10708
  hadDeltas: false,
10563
10709
  terminal: false,
10710
+ stopped: false,
10564
10711
  workspaceRefreshKey: 0,
10565
10712
  browser: null,
10566
10713
  viewMode: options.viewMode ?? "simple",
@@ -10575,13 +10722,13 @@ function applyAgentChatEvent(state, event) {
10575
10722
  sessionDone: state.sessionDone || event.type === "session.done"
10576
10723
  };
10577
10724
  nextState = applyRetryIndicator(nextState, event);
10578
- if (state.terminal && isPostTerminalLiveEvent(event.type)) {
10725
+ if (state.stopped && isPostTerminalLiveEvent(event.type)) {
10579
10726
  return nextState;
10580
10727
  }
10581
10728
  switch (event.type) {
10582
10729
  case "user.message":
10583
10730
  return withMessages(
10584
- { ...nextState, terminal: false, isRunning: true },
10731
+ { ...nextState, terminal: false, stopped: false, isRunning: true },
10585
10732
  applyUserMessage(nextState.messages, event.eventId, event.data)
10586
10733
  );
10587
10734
  case "skill.invoked":
@@ -11204,13 +11351,21 @@ function buildErrorInfo(event) {
11204
11351
  if (event.type !== "session.fail" || typeof event.data.error_category !== "string") {
11205
11352
  return void 0;
11206
11353
  }
11354
+ const detail = stringValue2(event.data.error_detail) || stringValue2(event.data.error);
11355
+ const insufficientCredits = isInsufficientCreditsError(detail);
11207
11356
  return {
11208
11357
  category: event.data.error_category,
11209
- title: stringValue2(event.data.error_title) || "The session failed due to an internal error.",
11210
- detail: stringValue2(event.data.error_detail) || stringValue2(event.data.error),
11211
- retryable: Boolean(event.data.retryable)
11358
+ title: insufficientCredits ? "You're out of credits" : stringValue2(event.data.error_title) || "The session failed due to an internal error.",
11359
+ detail,
11360
+ // A credit shortfall is not a transient failure — retrying just hits
11361
+ // the same 402. The billing CTA replaces Retry.
11362
+ retryable: insufficientCredits ? false : Boolean(event.data.retryable),
11363
+ insufficientCredits
11212
11364
  };
11213
11365
  }
11366
+ function isInsufficientCreditsError(text) {
11367
+ return typeof text === "string" && text.includes("insufficient_credits");
11368
+ }
11214
11369
  function applyExpertResult(messages, eventId, data) {
11215
11370
  const match = findLatestConsultExpertCall(messages);
11216
11371
  if (!match) return messages;
@@ -11610,6 +11765,15 @@ function readTurnMeta(data) {
11610
11765
  }
11611
11766
 
11612
11767
  // src/runtime/use-agent-chat-runtime.ts
11768
+ var RECONCILE_FIRST_DELAY = 4e3;
11769
+ var RECONCILE_FAST = 1500;
11770
+ var RECONCILE_ACTIVE = 4e3;
11771
+ var RECONCILE_MISSION_IDLE = 5e3;
11772
+ var RECONCILE_IDLE = 15e3;
11773
+ function applyRuntimeEvent(prev, event) {
11774
+ if (event.eventId > 0 && event.eventId <= prev.lastEventId) return prev;
11775
+ return applyAgentChatEvent(prev, event);
11776
+ }
11613
11777
  var VIEW_MODE_KEY = "@invergent/agent-chat-react:viewMode";
11614
11778
  var VIEW_MODE_EVENT = "@invergent/agent-chat-react:viewMode:change";
11615
11779
  function readPersistedViewMode() {
@@ -11677,6 +11841,7 @@ function useAgentChatRuntime({
11677
11841
  const stateRef = useRef(state);
11678
11842
  const streamRef = useRef(null);
11679
11843
  const reconnectTimerRef = useRef(null);
11844
+ const reconcileTimerRef = useRef(null);
11680
11845
  const previousSessionIdRef = useRef(sessionId);
11681
11846
  const sessionIdRef = useRef(sessionId);
11682
11847
  useEffect(() => {
@@ -11709,7 +11874,7 @@ function useAgentChatRuntime({
11709
11874
  const data = parseEventData(messageEvent.data);
11710
11875
  const eventId = messageEvent.lastEventId ? Number(messageEvent.lastEventId) : 0;
11711
11876
  setState(
11712
- (prev) => applyAgentChatEvent(prev, {
11877
+ (prev) => applyRuntimeEvent(prev, {
11713
11878
  type: eventType,
11714
11879
  eventId,
11715
11880
  data
@@ -11741,12 +11906,110 @@ function useAgentChatRuntime({
11741
11906
  stateRef.current = {
11742
11907
  ...stateRef.current,
11743
11908
  sessionDone: false,
11744
- terminal: false
11909
+ terminal: false,
11910
+ stopped: false
11745
11911
  };
11746
11912
  openStream(targetSessionId, after);
11747
11913
  },
11748
11914
  [clearReconnectTimer, closeStream, openStream]
11749
11915
  );
11916
+ const reviveStreamIfNeeded = useCallback(
11917
+ (targetSessionId) => {
11918
+ if (streamRef.current) return;
11919
+ if (reconnectTimerRef.current) return;
11920
+ stateRef.current = { ...stateRef.current, sessionDone: false };
11921
+ setState(
11922
+ (prev) => prev.sessionDone ? { ...prev, sessionDone: false } : prev
11923
+ );
11924
+ openStream(targetSessionId, stateRef.current.lastEventId);
11925
+ },
11926
+ [openStream]
11927
+ );
11928
+ const applyPolledEvents = useCallback((events) => {
11929
+ if (events.length === 0) return;
11930
+ setState((prev) => {
11931
+ let next = prev;
11932
+ for (const event of events) {
11933
+ if (!AGENT_CHAT_LISTENED_EVENT_SET.has(event.type)) continue;
11934
+ next = applyRuntimeEvent(next, {
11935
+ type: event.type,
11936
+ eventId: event.id,
11937
+ data: event.data ?? {}
11938
+ });
11939
+ }
11940
+ return next;
11941
+ });
11942
+ }, []);
11943
+ const reconcileTick = useCallback(async () => {
11944
+ const targetSessionId = sessionIdRef.current;
11945
+ if (!targetSessionId) return RECONCILE_IDLE;
11946
+ let appliedNew = false;
11947
+ if (adapter.pollEvents) {
11948
+ let cursor = stateRef.current.lastEventId;
11949
+ try {
11950
+ for (let page = 0; page < 5; page++) {
11951
+ const res = await adapter.pollEvents({
11952
+ sessionId: targetSessionId,
11953
+ after: cursor,
11954
+ limit: 200
11955
+ });
11956
+ if (sessionIdRef.current !== targetSessionId) return RECONCILE_IDLE;
11957
+ const events = res.events ?? [];
11958
+ if (events.length === 0) break;
11959
+ const fresh = events.filter((event) => event.id > cursor);
11960
+ const known = fresh.filter(
11961
+ (event) => AGENT_CHAT_LISTENED_EVENT_SET.has(event.type)
11962
+ );
11963
+ if (known.length > 0) {
11964
+ applyPolledEvents(known);
11965
+ appliedNew = true;
11966
+ }
11967
+ for (const event of events) cursor = Math.max(cursor, event.id);
11968
+ if (!res.hasMore) break;
11969
+ }
11970
+ } catch {
11971
+ }
11972
+ }
11973
+ let live = false;
11974
+ let isMission = false;
11975
+ let statusKnown = false;
11976
+ try {
11977
+ const session2 = await adapter.getSession({ sessionId: targetSessionId });
11978
+ if (sessionIdRef.current !== targetSessionId) return RECONCILE_IDLE;
11979
+ statusKnown = true;
11980
+ live = session2.status === "active";
11981
+ isMission = Boolean(
11982
+ session2.config?.active_mission_id
11983
+ );
11984
+ } catch {
11985
+ }
11986
+ if (live) reviveStreamIfNeeded(targetSessionId);
11987
+ if (appliedNew) return RECONCILE_FAST;
11988
+ if (live || !statusKnown) return RECONCILE_ACTIVE;
11989
+ return isMission || stateRef.current.isRunning ? RECONCILE_MISSION_IDLE : RECONCILE_IDLE;
11990
+ }, [adapter, applyPolledEvents, reviveStreamIfNeeded]);
11991
+ useEffect(() => {
11992
+ if (!sessionId) return;
11993
+ let cancelled = false;
11994
+ const schedule = (delay) => {
11995
+ if (cancelled) return;
11996
+ reconcileTimerRef.current = setTimeout(() => {
11997
+ if (cancelled) return;
11998
+ void reconcileTick().catch(() => RECONCILE_ACTIVE).then((next) => {
11999
+ if (cancelled) return;
12000
+ schedule(next);
12001
+ });
12002
+ }, delay);
12003
+ };
12004
+ schedule(RECONCILE_FIRST_DELAY);
12005
+ return () => {
12006
+ cancelled = true;
12007
+ if (reconcileTimerRef.current) {
12008
+ clearTimeout(reconcileTimerRef.current);
12009
+ reconcileTimerRef.current = null;
12010
+ }
12011
+ };
12012
+ }, [sessionId, reconcileTick]);
11750
12013
  useEffect(() => {
11751
12014
  const previousSessionId = previousSessionIdRef.current;
11752
12015
  previousSessionIdRef.current = sessionId;
@@ -11871,6 +12134,10 @@ function useAgentChatRuntime({
11871
12134
  return {
11872
12135
  ...prev,
11873
12136
  terminal: true,
12137
+ // User-initiated stop: gate the aborted turn's late in-flight
12138
+ // events (a natural session.complete leaves this false so an
12139
+ // autonomous mission resume still streams).
12140
+ stopped: true,
11874
12141
  isRunning: false,
11875
12142
  messages
11876
12143
  };
@@ -11953,6 +12220,7 @@ function useAgentChatRuntime({
11953
12220
  setState((prev) => ({
11954
12221
  ...prev,
11955
12222
  terminal: false,
12223
+ stopped: false,
11956
12224
  retryIndicator: null,
11957
12225
  isRunning: true
11958
12226
  }));
@@ -12080,8 +12348,10 @@ function AgentChat({
12080
12348
  disabled,
12081
12349
  onComposerError,
12082
12350
  deepResearchEnabled = false,
12351
+ researchEnabled = false,
12083
12352
  codeAgentsEnabled = false,
12084
- onOpenIntegrations
12353
+ onOpenIntegrations,
12354
+ onOpenBilling
12085
12355
  }) {
12086
12356
  const [workspacePath, setWorkspacePath] = useState(null);
12087
12357
  const [mobileView, setMobileView] = useState("chat");
@@ -12140,7 +12410,8 @@ function AgentChat({
12140
12410
  value: {
12141
12411
  adapter,
12142
12412
  sessionId,
12143
- onFileSelect: handleFileSelect
12413
+ onFileSelect: handleFileSelect,
12414
+ onOpenBilling
12144
12415
  },
12145
12416
  children: /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(
12146
12417
  "section",
@@ -12230,6 +12501,7 @@ function AgentChat({
12230
12501
  viewMode: runtime.viewMode,
12231
12502
  onViewModeChange: runtime.setViewMode,
12232
12503
  deepResearchEnabled,
12504
+ researchEnabled,
12233
12505
  codeAgentsEnabled,
12234
12506
  researchSources: runtime.researchSources,
12235
12507
  hideTurnSummary,
@@ -13902,7 +14174,7 @@ function ActivityRow({
13902
14174
  /* @__PURE__ */ jsx("p", { className: "shrink-0 text-xs text-foreground font-mono", children: missionEventActorLabel(event, feed.sessions) }),
13903
14175
  taskId ? /* @__PURE__ */ jsx("p", { className: "shrink-0 rounded bg-muted px-1.5 py-0.5 font-mono text-[9px] text-muted-foreground/70", children: taskId.slice(0, 8) }) : null
13904
14176
  ] }),
13905
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1 truncate text-foreground/80", children: missionEventSummary(event) }) })
14177
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1 truncate text-foreground/80", children: renderInlineMarkdown(missionEventSummary(event)) }) })
13906
14178
  ] })
13907
14179
  ] });
13908
14180
  }
@@ -13985,6 +14257,182 @@ function MissionMetadataTab({ mission }) {
13985
14257
  ] })
13986
14258
  ] });
13987
14259
  }
14260
+ function compareNodes(a, b) {
14261
+ if (a.nodeKey === b.nodeKey) return 0;
14262
+ if (a.nodeKey === "ROOT") return -1;
14263
+ if (b.nodeKey === "ROOT") return 1;
14264
+ const pa = a.nodeKey.split(".").map((p) => Number.parseInt(p, 10));
14265
+ const pb = b.nodeKey.split(".").map((p) => Number.parseInt(p, 10));
14266
+ const n = Math.min(pa.length, pb.length);
14267
+ for (let i = 0; i < n; i += 1) {
14268
+ if (pa[i] !== pb[i]) return pa[i] - pb[i];
14269
+ }
14270
+ return pa.length - pb.length;
14271
+ }
14272
+ function statusToneClass(status) {
14273
+ switch (status) {
14274
+ case "merged":
14275
+ return "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400";
14276
+ case "done":
14277
+ return "border-sky-500/30 bg-sky-500/10 text-sky-700 dark:text-sky-400";
14278
+ case "running":
14279
+ return "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400";
14280
+ case "failed":
14281
+ return "border-destructive/30 bg-destructive/10 text-destructive";
14282
+ case "pruned":
14283
+ return "border-border bg-muted/40 text-muted-foreground/70 line-through";
14284
+ default:
14285
+ return "border-border bg-muted/30 text-muted-foreground/80";
14286
+ }
14287
+ }
14288
+ function fmtScore(n) {
14289
+ if (n === null || n === void 0) return "\u2014";
14290
+ return n.toFixed(3).replace(/\.?0+$/, "") || "0";
14291
+ }
14292
+ function deltaLabel(score, ref, direction) {
14293
+ if (score === null || score === void 0 || ref === null || ref === void 0) {
14294
+ return null;
14295
+ }
14296
+ const raw = score - ref;
14297
+ const good = direction === "minimize" ? raw < 0 : raw > 0;
14298
+ const sign = raw > 0 ? "+" : raw < 0 ? "\u2212" : "\xB1";
14299
+ return { text: `${sign}${fmtScore(Math.abs(raw))}`, good };
14300
+ }
14301
+ function ScorePair({
14302
+ label,
14303
+ from,
14304
+ to,
14305
+ direction
14306
+ }) {
14307
+ const delta = deltaLabel(to, from, direction);
14308
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
14309
+ /* @__PURE__ */ jsx("div", { className: "font-mono text-[10px] uppercase tracking-widest text-muted-foreground/60", children: label }),
14310
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-1.5 tabular-nums", children: [
14311
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/70", children: fmtScore(from) }),
14312
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/40", children: "\u2192" }),
14313
+ /* @__PURE__ */ jsx("span", { className: "text-lg font-semibold text-foreground", children: fmtScore(to) }),
14314
+ delta ? /* @__PURE__ */ jsx(
14315
+ "span",
14316
+ {
14317
+ className: delta.good ? "text-xs font-medium text-emerald-600 dark:text-emerald-400" : "text-xs font-medium text-muted-foreground/60",
14318
+ children: delta.text
14319
+ }
14320
+ ) : null
14321
+ ] })
14322
+ ] });
14323
+ }
14324
+ function RunHeader({ run }) {
14325
+ return /* @__PURE__ */ jsxs("section", { className: "space-y-4 rounded-md border bg-card/40 p-4", children: [
14326
+ run.objective ? /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground/90", children: run.objective }) : null,
14327
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
14328
+ /* @__PURE__ */ jsx(
14329
+ ScorePair,
14330
+ {
14331
+ label: "Dev (working)",
14332
+ from: run.baselineScore,
14333
+ to: run.trunkScore ?? run.baselineScore,
14334
+ direction: run.metricDirection
14335
+ }
14336
+ ),
14337
+ /* @__PURE__ */ jsx(
14338
+ ScorePair,
14339
+ {
14340
+ label: "Held-out test (authoritative)",
14341
+ from: run.testBaselineScore,
14342
+ to: run.testTrunkScore ?? run.testBaselineScore,
14343
+ direction: run.metricDirection
14344
+ }
14345
+ )
14346
+ ] }),
14347
+ /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-[11px]", children: [
14348
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground/60", children: "Status" }),
14349
+ /* @__PURE__ */ jsx("dd", { className: "text-foreground/80", children: run.status }),
14350
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground/60", children: "Repo" }),
14351
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("code", { className: "text-[11px]", children: run.repoPath }) }),
14352
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground/60", children: "Trunk" }),
14353
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("code", { className: "text-[11px]", children: run.trunkBranch }) }),
14354
+ run.evalCmd ? /* @__PURE__ */ jsxs(Fragment, { children: [
14355
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground/60", children: "Dev eval" }),
14356
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("code", { className: "text-[11px]", children: run.evalCmd }) })
14357
+ ] }) : null,
14358
+ run.evalCmdTest ? /* @__PURE__ */ jsxs(Fragment, { children: [
14359
+ /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground/60", children: "Held-out eval" }),
14360
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("code", { className: "text-[11px]", children: run.evalCmdTest }) })
14361
+ ] }) : null
14362
+ ] })
14363
+ ] });
14364
+ }
14365
+ function IdeaRow({
14366
+ node,
14367
+ baseline,
14368
+ direction
14369
+ }) {
14370
+ const indent = node.nodeKey === "ROOT" ? 0 : node.depth;
14371
+ const delta = deltaLabel(node.score, baseline, direction);
14372
+ const headline = (node.hypothesis || "").split("\n")[0];
14373
+ const insight = node.insight ? node.insight.split("\n").slice(-1)[0] : null;
14374
+ return /* @__PURE__ */ jsxs(
14375
+ "li",
14376
+ {
14377
+ className: "flex flex-col gap-0.5 border-b border-border/40 py-2 last:border-b-0",
14378
+ style: { paddingLeft: `${indent * 16}px` },
14379
+ children: [
14380
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
14381
+ /* @__PURE__ */ jsx("code", { className: "shrink-0 text-[11px] text-muted-foreground/60", children: node.nodeKey }),
14382
+ /* @__PURE__ */ jsx(
14383
+ "span",
14384
+ {
14385
+ className: `shrink-0 rounded border px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-wider ${statusToneClass(node.status)}`,
14386
+ children: node.status
14387
+ }
14388
+ ),
14389
+ node.score !== null && node.score !== void 0 ? /* @__PURE__ */ jsxs("span", { className: "shrink-0 tabular-nums text-xs font-medium text-foreground/90", children: [
14390
+ fmtScore(node.score),
14391
+ delta ? /* @__PURE__ */ jsxs(
14392
+ "span",
14393
+ {
14394
+ className: delta.good ? " text-emerald-600 dark:text-emerald-400" : " text-muted-foreground/50",
14395
+ children: [
14396
+ " ",
14397
+ delta.text
14398
+ ]
14399
+ }
14400
+ ) : null
14401
+ ] }) : null,
14402
+ /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-foreground/80", title: headline, children: headline })
14403
+ ] }),
14404
+ insight ? /* @__PURE__ */ jsx("p", { className: "truncate pl-[calc(1.5rem)] text-[11px] text-muted-foreground/60", title: node.insight ?? void 0, children: insight }) : null
14405
+ ]
14406
+ }
14407
+ );
14408
+ }
14409
+ function MissionResearchTab({ research }) {
14410
+ const sorted = useMemo(
14411
+ () => [...research.nodes].sort(compareNodes),
14412
+ [research.nodes]
14413
+ );
14414
+ const { run } = research;
14415
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
14416
+ /* @__PURE__ */ jsx(RunHeader, { run }),
14417
+ /* @__PURE__ */ jsxs("section", { children: [
14418
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 font-mono text-[10px] uppercase tracking-widest text-muted-foreground/70", children: [
14419
+ "Idea Tree \xB7 ",
14420
+ sorted.length,
14421
+ " node",
14422
+ sorted.length === 1 ? "" : "s"
14423
+ ] }),
14424
+ sorted.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground/60", children: "No hypotheses yet." }) : /* @__PURE__ */ jsx("ul", { className: "rounded-md border", children: sorted.map((n) => /* @__PURE__ */ jsx(
14425
+ IdeaRow,
14426
+ {
14427
+ node: n,
14428
+ baseline: run.baselineScore,
14429
+ direction: run.metricDirection
14430
+ },
14431
+ n.nodeKey
14432
+ )) })
14433
+ ] })
14434
+ ] });
14435
+ }
13988
14436
  var TASK_STATUS_BADGE_CLASS = {
13989
14437
  running: "border-primary/30 bg-primary/10 text-primary",
13990
14438
  blocked: "border-amber-500/30 bg-amber-500/10 text-amber-600",
@@ -14190,7 +14638,7 @@ function TaskDetail({
14190
14638
  /* @__PURE__ */ jsx("span", { className: "w-16 shrink-0 font-mono text-[10px] text-muted-foreground/60", children: formatMissionTimestamp(e.createdAt) }),
14191
14639
  /* @__PURE__ */ jsxs("div", { className: "", children: [
14192
14640
  /* @__PURE__ */ jsx("p", { className: "shrink-0 font-mono uppercase text-muted-foreground", children: missionEventCategory(e.type) }),
14193
- /* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1 text-foreground/80", children: missionEventSummary(e) })
14641
+ /* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1 text-foreground/80", children: renderInlineMarkdown(missionEventSummary(e)) })
14194
14642
  ] })
14195
14643
  ] }, e.id)) })
14196
14644
  ] }) : null,
@@ -14303,7 +14751,7 @@ function OutcomeCard({ outcome }) {
14303
14751
  ),
14304
14752
  outcome.timestamp ? /* @__PURE__ */ jsx(CardAction, { className: "font-mono text-[10px] text-muted-foreground/60", children: formatMissionTimestamp(outcome.timestamp) }) : null
14305
14753
  ] }),
14306
- /* @__PURE__ */ jsx(CardContent, { children: outcome.markdown ? /* @__PURE__ */ jsx(MessageResponse, { className: "font-mono text-xs leading-relaxed text-foreground/90", children: outcome.markdown }) : /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap font-mono text-xs leading-relaxed text-foreground/90", children: outcome.text }) })
14754
+ /* @__PURE__ */ jsx(CardContent, { children: outcome.markdown ? /* @__PURE__ */ jsx(MessageResponse, { className: "font-mono text-xs leading-relaxed text-foreground/90", children: outcome.markdown }) : /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap font-mono text-xs leading-relaxed text-foreground/90", children: renderInlineMarkdown(outcome.text) }) })
14307
14755
  ]
14308
14756
  }
14309
14757
  );
@@ -14473,6 +14921,7 @@ var INITIAL_STATE = {
14473
14921
  mission: null,
14474
14922
  tasks: [],
14475
14923
  workers: [],
14924
+ research: null,
14476
14925
  loading: true,
14477
14926
  error: null
14478
14927
  };
@@ -14515,16 +14964,19 @@ function MissionDashboard({
14515
14964
  }, []);
14516
14965
  const refresh = useCallback(async () => {
14517
14966
  try {
14518
- const [mission2, tasksResp, workersResp] = await Promise.all([
14967
+ const researchFn = adapter.getMissionResearch;
14968
+ const [mission2, tasksResp, workersResp, research] = await Promise.all([
14519
14969
  api.getMission({ missionId }),
14520
14970
  api.getMissionTasks({ missionId }),
14521
- api.getMissionWorkers({ missionId })
14971
+ api.getMissionWorkers({ missionId }),
14972
+ researchFn ? researchFn({ missionId }).catch(() => null) : Promise.resolve(null)
14522
14973
  ]);
14523
14974
  if (!isMounted.current) return;
14524
14975
  setState({
14525
14976
  mission: mission2,
14526
14977
  tasks: tasksResp.tasks,
14527
14978
  workers: workersResp.workers,
14979
+ research,
14528
14980
  loading: false,
14529
14981
  error: null
14530
14982
  });
@@ -14536,7 +14988,7 @@ function MissionDashboard({
14536
14988
  error: err instanceof Error ? err.message : String(err)
14537
14989
  }));
14538
14990
  }
14539
- }, [api, missionId]);
14991
+ }, [adapter, api, missionId]);
14540
14992
  useEffect(() => {
14541
14993
  void refresh();
14542
14994
  }, [refresh]);
@@ -14735,6 +15187,10 @@ function MissionDashboard({
14735
15187
  className: "mt-5 flex flex-1 flex-col overflow-hidden sm:mt-6",
14736
15188
  children: [
14737
15189
  /* @__PURE__ */ jsx("div", { className: "shrink-0 border-b border-border bg-background px-5 sm:px-6", children: /* @__PURE__ */ jsxs(TabsList, { variant: "line", children: [
15190
+ state.research ? /* @__PURE__ */ jsxs(TabsTrigger, { value: "research", children: [
15191
+ "Research",
15192
+ /* @__PURE__ */ jsx(CountBadge, { n: state.research.nodes.length })
15193
+ ] }) : null,
14738
15194
  /* @__PURE__ */ jsxs(TabsTrigger, { value: "tasks", children: [
14739
15195
  "Tasks",
14740
15196
  /* @__PURE__ */ jsx(CountBadge, { n: state.tasks.length })
@@ -14750,6 +15206,7 @@ function MissionDashboard({
14750
15206
  /* @__PURE__ */ jsx(TabsTrigger, { value: "metadata", children: "Metadata" })
14751
15207
  ] }) }),
14752
15208
  /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto bg-background px-5 py-4 sm:px-6", children: [
15209
+ state.research ? /* @__PURE__ */ jsx(TabsContent, { value: "research", className: "mt-0", children: /* @__PURE__ */ jsx(MissionResearchTab, { research: state.research }) }) : null,
14753
15210
  /* @__PURE__ */ jsx(TabsContent, { value: "tasks", className: "mt-0 h-full", children: /* @__PURE__ */ jsx(
14754
15211
  MissionTasksTab,
14755
15212
  {