@blade-hq/agent-react 2610.0.0-beta.34 → 2610.0.0-beta.35

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.
@@ -60,6 +60,12 @@ interface ChatSurfaceProps extends ChatPresentationProps {
60
60
  showPlanUpdates?: boolean;
61
61
  /** 仅实时成功的 update_plan 递增;历史 hydration 不得改变。 */
62
62
  planRevealRevision?: number;
63
+ historyPaging?: {
64
+ hasOlder: boolean;
65
+ loading: boolean;
66
+ /** Resolves true only when the paging cursor advanced. */
67
+ loadOlder: (beforeCommit?: () => void) => Promise<boolean>;
68
+ };
63
69
  /** 连接状态条下方的横幅(回放模式条用)。不传时不渲染任何节点。 */
64
70
  banner?: ReactNode;
65
71
  }
@@ -69,5 +75,5 @@ interface ChatSurfaceProps extends ChatPresentationProps {
69
75
  * 这里的 DOM 结构受 tests/chat-view-compat.test.tsx 的快照保护——接入方拿
70
76
  * .blade-chat-* 类名写了自己的样式,结构一动他们的页面就歪。
71
77
  */
72
- export declare function ChatSurface({ theme, classNames, renderers, slots, placeholder, connection, errorMessage, messages, postChatFollowup, isStreaming, isStopping, inputText, onInputChange, onSuggestion, onSend, onAppend, onStop, sessionStatus, askAnswers, onAnswer, sessionId, isViewer, resultFeedbackByEntry, onResultFeedbackSaved, onFollowupInteraction, beforeInput, showPlanUpdates, planRevealRevision, banner, }: ChatSurfaceProps): import("react/jsx-runtime").JSX.Element;
78
+ export declare function ChatSurface({ theme, classNames, renderers, slots, placeholder, connection, errorMessage, messages, postChatFollowup, isStreaming, isStopping, inputText, onInputChange, onSuggestion, onSend, onAppend, onStop, sessionStatus, askAnswers, onAnswer, sessionId, isViewer, resultFeedbackByEntry, onResultFeedbackSaved, onFollowupInteraction, beforeInput, showPlanUpdates, planRevealRevision, historyPaging, banner, }: ChatSurfaceProps): import("react/jsx-runtime").JSX.Element;
73
79
  export {};
@@ -20,10 +20,16 @@ interface Props {
20
20
  onFollowupInteraction?: (event: FollowupInteractionEvent) => void;
21
21
  resultFeedbackByEntry?: ReadonlyMap<string, ResultFeedback>;
22
22
  onResultFeedbackSaved?: (feedback: ResultFeedback) => void;
23
+ historyPaging?: {
24
+ hasOlder: boolean;
25
+ loading: boolean;
26
+ /** Resolves true only when the paging cursor advanced. */
27
+ loadOlder: (beforeCommit?: () => void) => Promise<boolean>;
28
+ };
23
29
  }
24
30
  /**
25
31
  * 消息列表:use-stick-to-bottom 自动滚动 + 按轮次分组渲染。
26
32
  * 相比第一方版本去掉了轮次导航栏、吸顶状态条与规划摘要卡片。
27
33
  */
28
- export declare function MessageList({ messages, postChatFollowup, onSuggestion, isStreaming, sessionStatus, askAnswers, onAnswer, toolCallRenderer, hidePlanUpdateTools, emptyState, className, sessionId, isViewer, onFollowupInteraction, resultFeedbackByEntry, onResultFeedbackSaved, }: Props): import("react/jsx-runtime").JSX.Element;
34
+ export declare function MessageList({ messages, postChatFollowup, onSuggestion, isStreaming, sessionStatus, askAnswers, onAnswer, toolCallRenderer, hidePlanUpdateTools, emptyState, className, sessionId, isViewer, onFollowupInteraction, resultFeedbackByEntry, onResultFeedbackSaved, historyPaging, }: Props): import("react/jsx-runtime").JSX.Element;
29
35
  export {};
@@ -27,6 +27,7 @@ declare const BladeAgent: {
27
27
  SessionHub: typeof agentClient.SessionHub;
28
28
  SessionSetupError: typeof agentClient.SessionSetupError;
29
29
  createInitialSessionState: typeof agentClient.createInitialSessionState;
30
+ reconcileHistoricalUserTurns: typeof agentClient.reconcileHistoricalUserTurns;
30
31
  reconcileOptimisticUserTurns: typeof agentClient.reconcileOptimisticUserTurns;
31
32
  toReplaySnapshot: typeof agentClient.toReplaySnapshot;
32
33
  connectEmbedded: typeof agentClient.connectEmbedded;
@@ -37,6 +38,7 @@ declare const BladeAgent: {
37
38
  computerState: typeof agentClient.computerState;
38
39
  sortComputers: typeof agentClient.sortComputers;
39
40
  ModelsResource: typeof agentClient.ModelsResource;
41
+ hasChatRunEvent: typeof agentClient.hasChatRunEvent;
40
42
  buildMessageContent: typeof agentClient.buildMessageContent;
41
43
  chatErrorForDisplay: typeof agentClient.chatErrorForDisplay;
42
44
  contentPreview: typeof agentClient.contentPreview;
@@ -53,6 +55,8 @@ declare const BladeAgent: {
53
55
  getContextGroupDisplayState: typeof agentClient.getContextGroupDisplayState;
54
56
  groupAdjacentContextRuns: typeof agentClient.groupAdjacentContextRuns;
55
57
  latestPostChatFollowup: typeof agentClient.latestPostChatFollowup;
58
+ prependOlder: typeof agentClient.prependOlder;
59
+ replaceWindow: typeof agentClient.replaceWindow;
56
60
  DEFAULT_REPLAY_SPEED: agentClient.ReplaySpeed;
57
61
  SessionInfo: import("arktype/internal/variants/object.ts").ObjectType<{
58
62
  id: string;
@@ -100,6 +104,7 @@ declare const BladeAgent: {
100
104
  }, {}>;
101
105
  TaskStatus: import("arktype/internal/variants/string.ts").StringType<"done" | "failed" | "in_progress" | "pending" | "skipped", {}>;
102
106
  acceptedPostChatFollowupCompletesLatestRun: typeof agentClient.acceptedPostChatFollowupCompletesLatestRun;
107
+ acknowledgeTurnEvents: typeof agentClient.acknowledgeTurnEvents;
103
108
  ClientProjectionBuilder: typeof agentClient.ClientProjectionBuilder;
104
109
  createSocket: typeof agentClient.createSocket;
105
110
  getChat: typeof getChat;
package/dist/index.js CHANGED
@@ -1144,7 +1144,7 @@ var X = createLucideIcon("X", [
1144
1144
  ]);
1145
1145
 
1146
1146
  // src/components/AgentChat.tsx
1147
- import { useCallback as useCallback8, useEffect as useEffect13, useMemo as useMemo8, useState as useState15 } from "react";
1147
+ import { useCallback as useCallback8, useEffect as useEffect12, useMemo as useMemo8, useState as useState15 } from "react";
1148
1148
 
1149
1149
  // src/lib/utils.ts
1150
1150
  function cn(...inputs) {
@@ -1613,7 +1613,7 @@ function CurrentPlanPanel({
1613
1613
  import { chatErrorForDisplay as chatErrorForDisplay2 } from "@blade-hq/agent-client";
1614
1614
 
1615
1615
  // src/components/ChatInput.tsx
1616
- import { useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
1616
+ import { useState as useState5 } from "react";
1617
1617
  import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1618
1618
  function isImeCompositionKey(event) {
1619
1619
  return event.isComposing || event.keyCode === 229;
@@ -1635,36 +1635,12 @@ function ChatInput({
1635
1635
  }) {
1636
1636
  const trimmed = value.trim();
1637
1637
  const [sendMode, setSendMode] = useState5("direct");
1638
- const [promptQueue, setPromptQueue] = useState5([]);
1639
- const queueSendingRef = useRef5(false);
1640
- const queueBlockedRef = useRef5(false);
1641
- const previousQueueKeyRef = useRef5(queueKey);
1642
- useEffect5(() => {
1643
- if (previousQueueKeyRef.current === queueKey) return;
1644
- previousQueueKeyRef.current = queueKey;
1645
- setPromptQueue([]);
1646
- queueBlockedRef.current = false;
1647
- }, [queueKey]);
1638
+ void queueKey;
1648
1639
  const canSend = trimmed.length > 0 && (!isStreaming || sendMode === "queue");
1649
- useEffect5(() => {
1650
- if (isStreaming) {
1651
- queueBlockedRef.current = false;
1652
- return;
1653
- }
1654
- if (queueBlockedRef.current || promptQueue.length === 0 || queueSendingRef.current) return;
1655
- const next = promptQueue[0];
1656
- queueSendingRef.current = true;
1657
- Promise.resolve().then(() => onSend(next)).then((accepted) => {
1658
- if (accepted) setPromptQueue((current) => current.slice(1));
1659
- else queueBlockedRef.current = true;
1660
- }).finally(() => {
1661
- queueSendingRef.current = false;
1662
- });
1663
- }, [isStreaming, onSend, promptQueue]);
1664
1640
  const handleSend = async () => {
1665
1641
  if (!canSend) return;
1666
1642
  if (isStreaming && sendMode === "queue") {
1667
- setPromptQueue((current) => [...current, trimmed]);
1643
+ if (onAppend) onAppend(trimmed);
1668
1644
  onValueChange("");
1669
1645
  return;
1670
1646
  }
@@ -1690,24 +1666,11 @@ function ChatInput({
1690
1666
  }
1691
1667
  };
1692
1668
  return /* @__PURE__ */ jsxs4("div", { className: cn("blade-chat-input border-t border-[hsl(var(--border))] py-3", className), children: [
1693
- /* @__PURE__ */ jsxs4("div", { className: "mx-auto mb-2 flex max-w-[748px] items-center justify-between px-1 text-xs text-[hsl(var(--muted-foreground))]", children: [
1694
- /* @__PURE__ */ jsxs4("fieldset", { className: "flex items-center gap-1 rounded-md border border-[hsl(var(--border))] p-0.5", children: [
1695
- /* @__PURE__ */ jsx5("legend", { className: "sr-only", children: "\u53D1\u9001\u65B9\u5F0F" }),
1696
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setSendMode("direct"), "aria-pressed": sendMode === "direct", disabled: isStreaming && !onAppend, className: `rounded px-2 py-1 ${sendMode === "direct" ? "bg-[hsl(var(--accent))] text-[hsl(var(--foreground))]" : ""}`, children: "\u76F4\u63A5\u63D2\u5165" }),
1697
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setSendMode("queue"), "aria-pressed": sendMode === "queue", className: `rounded px-2 py-1 ${sendMode === "queue" ? "bg-[hsl(var(--accent))] text-[hsl(var(--foreground))]" : ""}`, children: "\u6392\u961F\u6267\u884C" })
1698
- ] }),
1699
- promptQueue.length > 0 ? /* @__PURE__ */ jsxs4("details", { className: "relative", children: [
1700
- /* @__PURE__ */ jsxs4("summary", { className: "cursor-pointer list-none rounded px-2 py-1 hover:bg-[hsl(var(--accent))]", children: [
1701
- "\u5F85\u6267\u884C ",
1702
- promptQueue.length,
1703
- " \u6761"
1704
- ] }),
1705
- /* @__PURE__ */ jsx5("div", { className: "absolute bottom-full right-0 z-20 mb-2 w-64 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-2 shadow-lg", children: promptQueue.map((item, index) => /* @__PURE__ */ jsxs4("div", { className: "flex gap-2 border-b border-[hsl(var(--border))] py-2 last:border-0", children: [
1706
- /* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate", children: item }),
1707
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setPromptQueue((current) => current.filter((_, i) => i !== index)), children: "\u53D6\u6D88" })
1708
- ] }, `${index}-${item}`)) })
1709
- ] }) : null
1710
- ] }),
1669
+ /* @__PURE__ */ jsx5("div", { className: "mx-auto mb-2 flex max-w-[748px] items-center justify-between px-1 text-xs text-[hsl(var(--muted-foreground))]", children: /* @__PURE__ */ jsxs4("fieldset", { className: "flex items-center gap-1 rounded-md border border-[hsl(var(--border))] p-0.5", children: [
1670
+ /* @__PURE__ */ jsx5("legend", { className: "sr-only", children: "\u53D1\u9001\u65B9\u5F0F" }),
1671
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setSendMode("direct"), "aria-pressed": sendMode === "direct", disabled: isStreaming && !onAppend, className: `rounded px-2 py-1 ${sendMode === "direct" ? "bg-[hsl(var(--accent))] text-[hsl(var(--foreground))]" : ""}`, children: "\u76F4\u63A5\u63D2\u5165" }),
1672
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setSendMode("queue"), "aria-pressed": sendMode === "queue", className: `rounded px-2 py-1 ${sendMode === "queue" ? "bg-[hsl(var(--accent))] text-[hsl(var(--foreground))]" : ""}`, children: "\u6392\u961F\u6267\u884C" })
1673
+ ] }) }),
1711
1674
  /* @__PURE__ */ jsxs4("div", { className: "blade-chat-input-inner mx-auto flex max-w-[748px] items-end gap-2 rounded-2xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-3 py-2", children: [
1712
1675
  /* @__PURE__ */ jsx5(
1713
1676
  "textarea",
@@ -1746,16 +1709,16 @@ function ChatInput({
1746
1709
  }
1747
1710
 
1748
1711
  // src/components/ConnectionBanner.tsx
1749
- import { useEffect as useEffect6, useRef as useRef6, useState as useState6 } from "react";
1712
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState6 } from "react";
1750
1713
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1751
1714
  var CONNECTION_NOTICE_DELAY_MS = 3e3;
1752
1715
  var CONNECTION_ERROR_DELAY_MS = 15e3;
1753
1716
  function useConnectionNoticePhase(connected) {
1754
1717
  const [phase, setPhase] = useState6("hidden");
1755
- const connectedRef = useRef6(connected);
1756
- const timersRef = useRef6([]);
1718
+ const connectedRef = useRef5(connected);
1719
+ const timersRef = useRef5([]);
1757
1720
  connectedRef.current = connected;
1758
- useEffect6(() => {
1721
+ useEffect5(() => {
1759
1722
  const clearTimers = () => {
1760
1723
  for (const timer of timersRef.current) clearTimeout(timer);
1761
1724
  timersRef.current = [];
@@ -1795,7 +1758,7 @@ function useConnectionNoticePhase(connected) {
1795
1758
  return phase;
1796
1759
  }
1797
1760
  function ConnectionBanner({ connection, className }) {
1798
- const hasConnectedRef = useRef6(connection === "connected" || connection === "reconnecting");
1761
+ const hasConnectedRef = useRef5(connection === "connected" || connection === "reconnecting");
1799
1762
  if (connection === "connected") hasConnectedRef.current = true;
1800
1763
  const connected = connection === "connected";
1801
1764
  const phase = useConnectionNoticePhase(connected);
@@ -1822,10 +1785,10 @@ function ConnectionBanner({ connection, className }) {
1822
1785
 
1823
1786
  // src/components/MessageList.tsx
1824
1787
  import { isHiddenInternalMessage } from "@blade-hq/agent-client";
1825
- import { useCallback as useCallback7, useEffect as useEffect12, useMemo as useMemo7, useRef as useRef13, useState as useState14 } from "react";
1788
+ import { useCallback as useCallback7, useEffect as useEffect11, useMemo as useMemo7, useRef as useRef12, useState as useState14 } from "react";
1826
1789
 
1827
1790
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/useStickToBottom.js
1828
- import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef7, useState as useState7 } from "react";
1791
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef6, useState as useState7 } from "react";
1829
1792
  var DEFAULT_SPRING_ANIMATION = {
1830
1793
  /**
1831
1794
  * A value from 0 to 1, on how much to damp the animation.
@@ -1865,7 +1828,7 @@ var useStickToBottom = (options = {}) => {
1865
1828
  const [escapedFromLock, updateEscapedFromLock] = useState7(false);
1866
1829
  const [isAtBottom, updateIsAtBottom] = useState7(options.initial !== false);
1867
1830
  const [isNearBottom, setIsNearBottom] = useState7(false);
1868
- const optionsRef = useRef7(null);
1831
+ const optionsRef = useRef6(null);
1869
1832
  optionsRef.current = options;
1870
1833
  const isSelecting = useCallback4(() => {
1871
1834
  if (!mouseDown) {
@@ -2169,11 +2132,11 @@ function mergeAnimations(...animations) {
2169
2132
 
2170
2133
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/StickToBottom.js
2171
2134
  import * as React from "react";
2172
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef8 } from "react";
2135
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect6, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef7 } from "react";
2173
2136
  var StickToBottomContext = createContext2(null);
2174
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect7;
2137
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect6;
2175
2138
  function StickToBottom({ instance, children, resize, initial, mass, damping, stiffness, targetScrollTop: currentTargetScrollTop, contextRef, ...props }) {
2176
- const customTargetScrollTop = useRef8(null);
2139
+ const customTargetScrollTop = useRef7(null);
2177
2140
  const targetScrollTop = React.useCallback((target, elements) => {
2178
2141
  const get = context?.targetScrollTop ?? currentTargetScrollTop;
2179
2142
  return get?.(target, elements) ?? target;
@@ -2255,7 +2218,7 @@ import {
2255
2218
  getTextContent,
2256
2219
  normalizeMessageContent
2257
2220
  } from "@blade-hq/agent-client";
2258
- import { useEffect as useEffect10, useRef as useRef11, useState as useState12 } from "react";
2221
+ import { useEffect as useEffect9, useRef as useRef10, useState as useState12 } from "react";
2259
2222
 
2260
2223
  // src/components/AgentLoopBlock.tsx
2261
2224
  import { useState as useState8 } from "react";
@@ -2320,9 +2283,9 @@ function AgentLoopBlock({ toolCall }) {
2320
2283
 
2321
2284
  // src/components/MarkdownContent.tsx
2322
2285
  import {
2323
- useEffect as useEffect8,
2286
+ useEffect as useEffect7,
2324
2287
  useMemo as useMemo5,
2325
- useRef as useRef9,
2288
+ useRef as useRef8,
2326
2289
  useState as useState9
2327
2290
  } from "react";
2328
2291
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -2340,10 +2303,10 @@ function normalizeAdjacentUrlFormatting(value) {
2340
2303
  return normalized.replace(/blade-url-protected-(\d+)-marker/g, (_, index) => protectedSegments[Number(index)]);
2341
2304
  }
2342
2305
  function CodeBlockPre({ children, node: _node, ...props }) {
2343
- const preRef = useRef9(null);
2306
+ const preRef = useRef8(null);
2344
2307
  const [copied, setCopied] = useState9(false);
2345
2308
  const [language, setLanguage] = useState9("");
2346
- useEffect8(() => {
2309
+ useEffect7(() => {
2347
2310
  const codeEl = preRef.current?.querySelector("code");
2348
2311
  setLanguage(codeEl?.className.match(/language-(\S+)/)?.[1] ?? "");
2349
2312
  }, []);
@@ -2417,7 +2380,7 @@ function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
2417
2380
  import { useState as useState11 } from "react";
2418
2381
 
2419
2382
  // src/components/AskUserQuestionBlock.tsx
2420
- import { useEffect as useEffect9, useMemo as useMemo6, useRef as useRef10, useState as useState10 } from "react";
2383
+ import { useEffect as useEffect8, useMemo as useMemo6, useRef as useRef9, useState as useState10 } from "react";
2421
2384
  import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2422
2385
  var CUSTOM_TEXTAREA_MAX_HEIGHT = 160;
2423
2386
  function resizeCustomTextarea(textarea) {
@@ -2426,12 +2389,12 @@ function resizeCustomTextarea(textarea) {
2426
2389
  textarea.style.overflowY = textarea.scrollHeight > CUSTOM_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
2427
2390
  }
2428
2391
  function useAutoResizeTextarea(value) {
2429
- const textareaRef = useRef10(null);
2430
- useEffect9(() => {
2392
+ const textareaRef = useRef9(null);
2393
+ useEffect8(() => {
2431
2394
  const textarea = textareaRef.current;
2432
2395
  if (textarea?.value === value) resizeCustomTextarea(textarea);
2433
2396
  }, [value]);
2434
- useEffect9(() => {
2397
+ useEffect8(() => {
2435
2398
  const textarea = textareaRef.current;
2436
2399
  if (!textarea || typeof ResizeObserver === "undefined") return;
2437
2400
  let previousWidth = textarea.clientWidth;
@@ -2461,7 +2424,7 @@ function AskUserQuestionBlock({
2461
2424
  const [usingCustom, setUsingCustom] = useState10(/* @__PURE__ */ new Set());
2462
2425
  const [note, setNote] = useState10("");
2463
2426
  const [submitted, setSubmitted] = useState10(false);
2464
- useEffect9(() => {
2427
+ useEffect8(() => {
2465
2428
  if (sessionStatus === "failed" || sessionStatus === "interrupted") {
2466
2429
  setSubmitted(false);
2467
2430
  }
@@ -3255,9 +3218,9 @@ function AssistantTurnBlock({
3255
3218
  const [displayMode, setDisplayMode] = useState12(
3256
3219
  () => isStreaming || hasActionableToolCall ? "detail" : "compact"
3257
3220
  );
3258
- const userSelectedDisplayModeRef = useRef11(false);
3259
- const wasStreamingRef = useRef11(isStreaming);
3260
- useEffect10(() => {
3221
+ const userSelectedDisplayModeRef = useRef10(false);
3222
+ const wasStreamingRef = useRef10(isStreaming);
3223
+ useEffect9(() => {
3261
3224
  if (wasStreamingRef.current && !isStreaming && !userSelectedDisplayModeRef.current) {
3262
3225
  setDisplayMode(hasActionableToolCall ? "detail" : "compact");
3263
3226
  }
@@ -3269,7 +3232,7 @@ function AssistantTurnBlock({
3269
3232
  const hasLiveStartTime = messages.some(
3270
3233
  (message) => message.timestamp != null && Number.isFinite(Date.parse(message.timestamp))
3271
3234
  );
3272
- useEffect10(() => {
3235
+ useEffect9(() => {
3273
3236
  if (!isStreaming || !hasLiveStartTime) return;
3274
3237
  const timer = window.setInterval(() => setClock(Date.now()), 1e3);
3275
3238
  return () => window.clearInterval(timer);
@@ -3579,7 +3542,7 @@ var RenderErrorBoundary = class extends Component {
3579
3542
  };
3580
3543
 
3581
3544
  // src/components/PostChatFollowupBlock.tsx
3582
- import { useCallback as useCallback6, useEffect as useEffect11, useRef as useRef12, useState as useState13 } from "react";
3545
+ import { useCallback as useCallback6, useEffect as useEffect10, useRef as useRef11, useState as useState13 } from "react";
3583
3546
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
3584
3547
  function emitInteraction(callback, event) {
3585
3548
  try {
@@ -3710,10 +3673,10 @@ function ResultFeedback({
3710
3673
  const [reason, setReason] = useState13(savedFeedback?.reason ?? null);
3711
3674
  const [saving, setSaving] = useState13(false);
3712
3675
  const [saveError, setSaveError] = useState13(false);
3713
- const reportedShown = useRef12(false);
3714
- const latestChoice = useRef12(null);
3676
+ const reportedShown = useRef11(false);
3677
+ const latestChoice = useRef11(null);
3715
3678
  const eligible = followup.feedback_eligible === true && Boolean(sessionId) && !isViewer;
3716
- useEffect11(() => {
3679
+ useEffect10(() => {
3717
3680
  if (!eligible || reportedShown.current) return;
3718
3681
  reportedShown.current = true;
3719
3682
  emitInteraction(onInteraction, {
@@ -3722,7 +3685,7 @@ function ResultFeedback({
3722
3685
  assistantEntryId: followup.assistant_entry_id
3723
3686
  });
3724
3687
  }, [eligible, followup.assistant_entry_id, onInteraction, sessionId]);
3725
- useEffect11(() => {
3688
+ useEffect10(() => {
3726
3689
  if (!savedFeedback || latestChoice.current) return;
3727
3690
  setSaved(savedFeedback);
3728
3691
  setHelpful(savedFeedback.helpful);
@@ -3835,13 +3798,13 @@ function PostChatFollowupBlock({
3835
3798
  onFeedbackSaved
3836
3799
  }) {
3837
3800
  const [expanded, setExpanded] = useState13(false);
3838
- const adopted = useRef12(/* @__PURE__ */ new Set());
3839
- const reportedSuggestions = useRef12(false);
3840
- const reportedArtifacts = useRef12(/* @__PURE__ */ new Set());
3841
- const openedArtifacts = useRef12(/* @__PURE__ */ new Set());
3801
+ const adopted = useRef11(/* @__PURE__ */ new Set());
3802
+ const reportedSuggestions = useRef11(false);
3803
+ const reportedArtifacts = useRef11(/* @__PURE__ */ new Set());
3804
+ const openedArtifacts = useRef11(/* @__PURE__ */ new Set());
3842
3805
  const artifacts = followup.final_artifacts ?? [];
3843
3806
  const visibleArtifacts = expanded ? artifacts : artifacts.slice(0, 3);
3844
- useEffect11(() => {
3807
+ useEffect10(() => {
3845
3808
  if (!reportedSuggestions.current && followup.suggestions.length > 0) {
3846
3809
  reportedSuggestions.current = true;
3847
3810
  emitInteraction(onInteraction, {
@@ -4150,7 +4113,8 @@ function MessageList({
4150
4113
  isViewer = false,
4151
4114
  onFollowupInteraction,
4152
4115
  resultFeedbackByEntry = /* @__PURE__ */ new Map(),
4153
- onResultFeedbackSaved
4116
+ onResultFeedbackSaved,
4117
+ historyPaging
4154
4118
  }) {
4155
4119
  const visibleRootMessages = messages.filter((message) => {
4156
4120
  if ((message.loop_name ?? "root") !== "root") return false;
@@ -4247,6 +4211,7 @@ function MessageList({
4247
4211
  resize: "instant",
4248
4212
  children: [
4249
4213
  /* @__PURE__ */ jsx17(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsxs15("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: [
4214
+ historyPaging && (historyPaging.hasOlder || historyPaging.loading) ? /* @__PURE__ */ jsx17(LoadOlderSentinel, { ...historyPaging }) : null,
4250
4215
  isStreaming && latestUserMessage && latestPromptPreview ? /* @__PURE__ */ jsx17(
4251
4216
  "button",
4252
4217
  {
@@ -4346,14 +4311,84 @@ function MessageList({
4346
4311
  )
4347
4312
  ] });
4348
4313
  }
4314
+ function LoadOlderSentinel({
4315
+ hasOlder,
4316
+ loading,
4317
+ loadOlder
4318
+ }) {
4319
+ const { contentRef, scrollRef } = useStickToBottomContext();
4320
+ const sentinelRef = useRef12(null);
4321
+ const stateRef = useRef12({ hasOlder, loading, loadOlder });
4322
+ stateRef.current = { hasOlder, loading, loadOlder };
4323
+ useEffect11(() => {
4324
+ const sentinel = sentinelRef.current;
4325
+ const scroller = scrollRef.current;
4326
+ if (!sentinel || !scroller || typeof IntersectionObserver === "undefined") return;
4327
+ let entered = false;
4328
+ let disposed = false;
4329
+ const firstVisible = () => {
4330
+ const top = scroller.getBoundingClientRect().top;
4331
+ for (const row of contentRef.current?.querySelectorAll("[data-entry-id]") ?? []) {
4332
+ const rect = row.getBoundingClientRect();
4333
+ if (rect.bottom >= top) return { id: row.dataset.entryId, offset: rect.top - top };
4334
+ }
4335
+ return null;
4336
+ };
4337
+ const load = async () => {
4338
+ if (disposed || !stateRef.current.hasOlder || stateRef.current.loading) return;
4339
+ const commitSnapshot = { anchor: null, height: scroller.scrollHeight };
4340
+ let advanced = false;
4341
+ try {
4342
+ advanced = await stateRef.current.loadOlder(() => {
4343
+ commitSnapshot.anchor = firstVisible();
4344
+ commitSnapshot.height = scroller.scrollHeight;
4345
+ });
4346
+ } catch {
4347
+ return;
4348
+ }
4349
+ if (disposed) return;
4350
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
4351
+ if (disposed) return;
4352
+ const anchor = commitSnapshot.anchor;
4353
+ const heightBefore = commitSnapshot.height;
4354
+ const anchored = anchor?.id ? Array.from(contentRef.current?.querySelectorAll("[data-entry-id]") ?? []).find((row) => row.dataset.entryId === anchor.id) : null;
4355
+ if (anchored && anchor) {
4356
+ scroller.scrollTop += anchored.getBoundingClientRect().top - scroller.getBoundingClientRect().top - anchor.offset;
4357
+ } else {
4358
+ scroller.scrollTop += scroller.scrollHeight - heightBefore;
4359
+ }
4360
+ if (!disposed && advanced && stateRef.current.hasOlder && !stateRef.current.loading && scroller.scrollHeight <= scroller.clientHeight) {
4361
+ await load();
4362
+ }
4363
+ };
4364
+ const observer = new IntersectionObserver(
4365
+ ([entry]) => {
4366
+ if (!entry?.isIntersecting) {
4367
+ entered = false;
4368
+ return;
4369
+ }
4370
+ if (entered) return;
4371
+ entered = true;
4372
+ void load();
4373
+ },
4374
+ { root: scroller, rootMargin: "160px 0px 0px" }
4375
+ );
4376
+ observer.observe(sentinel);
4377
+ return () => {
4378
+ disposed = true;
4379
+ observer.disconnect();
4380
+ };
4381
+ }, [contentRef, scrollRef]);
4382
+ return /* @__PURE__ */ jsx17("div", { ref: sentinelRef, "data-history-sentinel": true, className: "flex h-6 items-center justify-center", children: loading ? /* @__PURE__ */ jsx17("span", { className: "text-xs text-[hsl(var(--muted-foreground))]", children: "\u6B63\u5728\u52A0\u8F7D\u66F4\u65E9\u6D88\u606F\u2026" }) : null });
4383
+ }
4349
4384
  function PinLatestUserMessage({
4350
4385
  userMessageCount,
4351
4386
  shouldPinLatestUser,
4352
4387
  targetKey
4353
4388
  }) {
4354
4389
  const { contentRef, scrollRef, scrollToBottom, stopScroll } = useStickToBottomContext();
4355
- const previousCountRef = useRef13(userMessageCount);
4356
- const spacerHeightRef = useRef13(0);
4390
+ const previousCountRef = useRef12(userMessageCount);
4391
+ const spacerHeightRef = useRef12(0);
4357
4392
  const getScrollElement = useCallback7(() => scrollRef.current, [scrollRef]);
4358
4393
  const getContentElement = useCallback7(() => contentRef.current, [contentRef]);
4359
4394
  const getTargetElement = useCallback7(() => {
@@ -4382,7 +4417,7 @@ function PinLatestUserMessage({
4382
4417
  stopAutoScroll: stopScroll,
4383
4418
  scrollToBottom
4384
4419
  });
4385
- useEffect12(() => {
4420
+ useEffect11(() => {
4386
4421
  if (userMessageCount > previousCountRef.current && !shouldPinLatestUser) {
4387
4422
  scrollToBottom("instant");
4388
4423
  }
@@ -4393,8 +4428,8 @@ function PinLatestUserMessage({
4393
4428
  function ScrollToBottomButton() {
4394
4429
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
4395
4430
  const [visible, setVisible] = useState14(false);
4396
- const hideTimerRef = useRef13(null);
4397
- useEffect12(() => {
4431
+ const hideTimerRef = useRef12(null);
4432
+ useEffect11(() => {
4398
4433
  if (isAtBottom) {
4399
4434
  if (!hideTimerRef.current) {
4400
4435
  hideTimerRef.current = setTimeout(() => {
@@ -4484,6 +4519,7 @@ function ChatSurface({
4484
4519
  beforeInput,
4485
4520
  showPlanUpdates = false,
4486
4521
  planRevealRevision = 0,
4522
+ historyPaging,
4487
4523
  banner
4488
4524
  }) {
4489
4525
  return /* @__PURE__ */ jsxs16(
@@ -4520,7 +4556,8 @@ function ChatSurface({
4520
4556
  isViewer,
4521
4557
  resultFeedbackByEntry,
4522
4558
  onResultFeedbackSaved,
4523
- onFollowupInteraction
4559
+ onFollowupInteraction,
4560
+ historyPaging
4524
4561
  }
4525
4562
  ),
4526
4563
  showPlanUpdates ? /* @__PURE__ */ jsx18(
@@ -4658,7 +4695,7 @@ function ChatSessionView({
4658
4695
  const [resultFeedback, setResultFeedback] = useState15([]);
4659
4696
  const resolvedSessionId = session?.sessionId;
4660
4697
  const isViewer = state?.viewerRole === "viewer";
4661
- useEffect13(() => {
4698
+ useEffect12(() => {
4662
4699
  setResultFeedback([]);
4663
4700
  if (!resolvedSessionId || isViewer) return;
4664
4701
  let cancelled = false;
@@ -4693,12 +4730,12 @@ function ChatSessionView({
4693
4730
  saved
4694
4731
  ]);
4695
4732
  }, []);
4696
- useEffect13(() => {
4733
+ useEffect12(() => {
4697
4734
  if (session) {
4698
4735
  onSessionReady?.(session);
4699
4736
  }
4700
4737
  }, [session, onSessionReady]);
4701
- useEffect13(() => {
4738
+ useEffect12(() => {
4702
4739
  if (!session) return;
4703
4740
  const offAttach = session.on("attachRequested", ({ label, content }) => {
4704
4741
  setInputText((prev) => `${prev ? `${prev}
@@ -4714,12 +4751,12 @@ ${content}`);
4714
4751
  offInsert();
4715
4752
  };
4716
4753
  }, [session]);
4717
- useEffect13(() => {
4754
+ useEffect12(() => {
4718
4755
  if (isUnauthorizedError(error)) {
4719
4756
  onUnauthorized();
4720
4757
  }
4721
4758
  }, [error, onUnauthorized]);
4722
- useEffect13(() => {
4759
+ useEffect12(() => {
4723
4760
  if (!session || !commands) return;
4724
4761
  const unsubscribes = Object.entries(commands).map(
4725
4762
  ([action, handler]) => session.onCommand(action, (payload) => handler(payload))
@@ -4772,13 +4809,22 @@ ${content}`);
4772
4809
  isStreaming,
4773
4810
  showPlanUpdates: true,
4774
4811
  planRevealRevision,
4812
+ historyPaging: session && state ? {
4813
+ hasOlder: session.hasOlderHistory,
4814
+ loading: state.loadingOlder,
4815
+ loadOlder: async (beforeCommit) => {
4816
+ const before = session.getState().nextBefore;
4817
+ await session.loadOlderHistory({ beforeCommit });
4818
+ return before !== session.getState().nextBefore;
4819
+ }
4820
+ } : void 0,
4775
4821
  isStopping,
4776
4822
  inputText,
4777
4823
  onInputChange: setInputText,
4778
4824
  onSuggestion: setInputText,
4779
4825
  onSend: handleSend,
4780
4826
  onAppend: (text) => {
4781
- void session?.send(text, { mode: state?.mode ?? void 0 });
4827
+ void session?.queue(text);
4782
4828
  },
4783
4829
  onStop: handleStop,
4784
4830
  sessionStatus: state?.status ?? void 0,
@@ -4796,7 +4842,7 @@ ${content}`);
4796
4842
  }
4797
4843
 
4798
4844
  // src/components/LlmChat.tsx
4799
- import { useEffect as useEffect14, useMemo as useMemo9, useState as useState17 } from "react";
4845
+ import { useEffect as useEffect13, useMemo as useMemo9, useState as useState17 } from "react";
4800
4846
 
4801
4847
  // src/components/LlmAdvancedSettings.tsx
4802
4848
  import { useState as useState16 } from "react";
@@ -4944,7 +4990,7 @@ ${text}` : text),
4944
4990
  }),
4945
4991
  [send, reset]
4946
4992
  );
4947
- useEffect14(() => {
4993
+ useEffect13(() => {
4948
4994
  onReady?.(handle);
4949
4995
  }, [handle, onReady]);
4950
4996
  return /* @__PURE__ */ jsx21(
@@ -5091,7 +5137,7 @@ function ContextGroupCard({ contexts, className }) {
5091
5137
  }
5092
5138
 
5093
5139
  // src/components/SessionMemoryToggle.tsx
5094
- import { useCallback as useCallback9, useEffect as useEffect15, useRef as useRef14, useState as useState18, useSyncExternalStore as useSyncExternalStore2 } from "react";
5140
+ import { useCallback as useCallback9, useEffect as useEffect14, useRef as useRef13, useState as useState18, useSyncExternalStore as useSyncExternalStore2 } from "react";
5095
5141
  import { jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
5096
5142
  var saveStates = /* @__PURE__ */ new WeakMap();
5097
5143
  function getSaveState(client, sessionId) {
@@ -5150,9 +5196,9 @@ function SessionMemoryToggle({
5150
5196
  getSaving
5151
5197
  );
5152
5198
  const [draftEnabled, setDraftEnabled] = useState18(enabled);
5153
- const activeSessionIdRef = useRef14(sessionId);
5199
+ const activeSessionIdRef = useRef13(sessionId);
5154
5200
  activeSessionIdRef.current = sessionId;
5155
- useEffect15(() => {
5201
+ useEffect14(() => {
5156
5202
  setDraftEnabled(enabled);
5157
5203
  }, [enabled, sessionId]);
5158
5204
  const update = useCallback9(