mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
feat(web): auto-follow streaming output and add scroll-to-bottom button
- ChatWindow: follow the streaming bubble on every chunk (scrollIntoView block:end), driven by followStreamRef (true by default, only genuine scroll gestures flip it) - Remove the agentRunning spacer that pushed content above the viewport at the bottom - Move the scroll-to-bottom button into the input row (ChatInput scrollToBottomButton slot), always visible, dimmed at the bottom; click jumps to the live stream or the end - useAgentSession: drop window pointerdown as scroll-intent marker (send-click was misread as a scroll-away, disabling follow before the stream started); expose atBottom/followStreamRef/scrollToBottom - i18n: add chat.scrollToBottom (en/zh-CN)
This commit is contained in:
@@ -23,7 +23,7 @@ Pi Agent Integrated 将 Pi 后端与 Pi Web 前端整合成一个可独立克隆
|
|||||||
| 浏览器自动化 | 需自行接入 | 无默认浏览器 MCP | Windows 默认使用 Edge;macOS 默认不启用,按需配置 |
|
| 浏览器自动化 | 需自行接入 | 无默认浏览器 MCP | Windows 默认使用 Edge;macOS 默认不启用,按需配置 |
|
||||||
| 恢复与长期状态 | 会话树与基础状态 | 会话 UI | Rewind 检查点、项目内 Memory、自动重试 |
|
| 恢复与长期状态 | 会话树与基础状态 | 会话 UI | Rewind 检查点、项目内 Memory、自动重试 |
|
||||||
| 多代理 | 核心能力可扩展 | 展示工具调用 | 配置了自动判断复杂度的 `pi-subagents` 策略 |
|
| 多代理 | 核心能力可扩展 | 展示工具调用 | 配置了自动判断复杂度的 `pi-subagents` 策略 |
|
||||||
| 本项目修复 | 不适用 | 上游行为 | 修复已结束会话状态 404、隐藏 Next.js 开发指示器、增加 Windows/macOS restart |
|
| 本项目修复 | 不适用 | 上游行为 | 修复已结束会话状态 404、隐藏 Next.js 开发指示器、增加 Windows/macOS restart、WebUI 流式输出自动跟踪与回到底部按钮 |
|
||||||
|
|
||||||
本仓库不是桌面安装包,也不是公网多用户服务。当前发布目标是:技术用户在 Windows 或 macOS 上克隆仓库、补充自己的模型凭据后,通过命令行启动一个隔离、可扩展的本地 Web Agent。
|
本仓库不是桌面安装包,也不是公网多用户服务。当前发布目标是:技术用户在 Windows 或 macOS 上克隆仓库、补充自己的模型凭据后,通过命令行启动一个隔离、可扩展的本地 Web Agent。
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ Pi Agent Integrated combines the Pi backend and Pi Web frontend into one indepen
|
|||||||
| Browser automation | User-integrated | No default browser MCP | Windows uses Edge by default; macOS leaves Playwright opt-in |
|
| Browser automation | User-integrated | No default browser MCP | Windows uses Edge by default; macOS leaves Playwright opt-in |
|
||||||
| Recovery and durable context | Session tree and core state | Session UI | Rewind checkpoints, repository-local memory, automatic retry |
|
| Recovery and durable context | Session tree and core state | Session UI | Rewind checkpoints, repository-local memory, automatic retry |
|
||||||
| Multi-agent workflow | Extensible core | Renders tool calls | Automatic complexity policy for `pi-subagents` |
|
| Multi-agent workflow | Extensible core | Renders tool calls | Automatic complexity policy for `pi-subagents` |
|
||||||
| Integration fixes | Not applicable | Upstream behavior | Handles ended-session state 404s, hides Next dev indicators, adds Windows/macOS restart |
|
| Integration fixes | Not applicable | Upstream behavior | Handles ended-session state 404s, hides Next dev indicators, adds Windows/macOS restart, and auto-follows streaming output with a scroll-to-bottom button in the Web UI |
|
||||||
|
|
||||||
This repository is not a desktop installer or a public multi-user service. Its current release target is a technical Windows or macOS user who clones the project, supplies personal model credentials, and starts an isolated, extensible local Web agent from the command line.
|
This repository is not a desktop installer or a public multi-user service. Its current release target is a technical Windows or macOS user who clones the project, supplies personal model credentials, and starts an isolated, extensible local Web agent from the command line.
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ interface Props {
|
|||||||
behavior: "steer" | "followUp",
|
behavior: "steer" | "followUp",
|
||||||
images?: AttachedImage[],
|
images?: AttachedImage[],
|
||||||
) => void;
|
) => void;
|
||||||
|
// Optional trailing control rendered inside the input row (right side,
|
||||||
|
// before the send/steer buttons) — used for the scroll-to-bottom button.
|
||||||
|
scrollToBottomButton?: React.ReactNode;
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
model?: { provider: string; modelId: string } | null;
|
model?: { provider: string; modelId: string } | null;
|
||||||
isAutoModelSelection?: boolean;
|
isAutoModelSelection?: boolean;
|
||||||
@@ -412,6 +415,7 @@ export const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput(
|
|||||||
onSoundToggle,
|
onSoundToggle,
|
||||||
onAudioUnlock,
|
onAudioUnlock,
|
||||||
onPromptWithStreamingBehavior,
|
onPromptWithStreamingBehavior,
|
||||||
|
scrollToBottomButton,
|
||||||
draftKey,
|
draftKey,
|
||||||
cwd,
|
cwd,
|
||||||
}: Props,
|
}: Props,
|
||||||
@@ -2212,6 +2216,8 @@ export const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput(
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{scrollToBottomButton}
|
||||||
|
|
||||||
{isStreaming ? (
|
{isStreaming ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -207,11 +207,14 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
isNew,
|
isNew,
|
||||||
sessionIdRef, messagesEndRef, scrollContainerRef,
|
sessionIdRef, messagesEndRef, scrollContainerRef,
|
||||||
lastUserMsgRef,
|
lastUserMsgRef,
|
||||||
|
atBottom,
|
||||||
|
followStreamRef,
|
||||||
handleSend, handleAbort, handleFork, handleNavigate, handleModelChange,
|
handleSend, handleAbort, handleFork, handleNavigate, handleModelChange,
|
||||||
handleCompact, handleSteer, handleFollowUp, handlePromptWithStreamingBehavior, handleAbortCompaction,
|
handleCompact, handleSteer, handleFollowUp, handlePromptWithStreamingBehavior, handleAbortCompaction,
|
||||||
handleRecallQueue,
|
handleRecallQueue,
|
||||||
handleBuiltinSlashCommand,
|
handleBuiltinSlashCommand,
|
||||||
handleToolPresetChange, handleThinkingLevelChange, loadSlashCommands,
|
handleToolPresetChange, handleThinkingLevelChange, loadSlashCommands,
|
||||||
|
scrollToBottom,
|
||||||
} = useAgentSession({
|
} = useAgentSession({
|
||||||
session, newSessionCwd, onAgentEnd: wrappedOnAgentEnd, onSessionCreated, onSessionForked,
|
session, newSessionCwd, onAgentEnd: wrappedOnAgentEnd, onSessionCreated, onSessionForked,
|
||||||
modelsRefreshKey, chatInputRef, onBranchDataChange, onSystemPromptChange, onSessionStatsPanelOpen,
|
modelsRefreshKey, chatInputRef, onBranchDataChange, onSystemPromptChange, onSessionStatsPanelOpen,
|
||||||
@@ -259,6 +262,30 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
container.scrollTop = restoreScrollTop(container.scrollHeight, prevScrollDistanceRef.current);
|
container.scrollTop = restoreScrollTop(container.scrollHeight, prevScrollDistanceRef.current);
|
||||||
prevScrollDistanceRef.current = null;
|
prevScrollDistanceRef.current = null;
|
||||||
}, [visibleCount, scrollContainerRef]);
|
}, [visibleCount, scrollContainerRef]);
|
||||||
|
|
||||||
|
// Live-follow the model's streaming output: pin the streaming bubble to
|
||||||
|
// the bottom of the viewport on every chunk while auto-follow is engaged
|
||||||
|
// (the default). Scrolling away from the bottom flips followStreamRef off
|
||||||
|
// inside useAgentSession; scrolling back to the bottom or clicking the
|
||||||
|
// scroll-to-bottom button re-engages it.
|
||||||
|
const streamBubbleRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!streamState.isStreaming || !streamState.streamingMessage) return;
|
||||||
|
if (!followStreamRef.current) return;
|
||||||
|
streamBubbleRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
|
||||||
|
}, [streamState]);
|
||||||
|
|
||||||
|
// Jump back to the newest content: scroll to the streaming bubble while a
|
||||||
|
// stream is live (its bottom edge is the live bottom), otherwise to the
|
||||||
|
// end of the conversation. Also re-engages auto-follow.
|
||||||
|
const handleScrollToConversationBottom = useCallback(() => {
|
||||||
|
followStreamRef.current = true;
|
||||||
|
if (streamState.isStreaming && streamState.streamingMessage) {
|
||||||
|
streamBubbleRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
|
||||||
|
} else {
|
||||||
|
scrollToBottom("smooth");
|
||||||
|
}
|
||||||
|
}, [streamState, followStreamRef, scrollToBottom]);
|
||||||
// Push session stats up to AppShell for the top bar.
|
// Push session stats up to AppShell for the top bar.
|
||||||
// Compare scalar fields to avoid loops from new object identity each render.
|
// Compare scalar fields to avoid loops from new object identity each render.
|
||||||
const statsKey = sessionStats
|
const statsKey = sessionStats
|
||||||
@@ -341,6 +368,36 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
onSteer={agentRunning ? handleSteer : undefined}
|
onSteer={agentRunning ? handleSteer : undefined}
|
||||||
onFollowUp={agentRunning ? handleFollowUp : undefined}
|
onFollowUp={agentRunning ? handleFollowUp : undefined}
|
||||||
onPromptWithStreamingBehavior={agentRunning ? handlePromptWithStreamingBehavior : undefined}
|
onPromptWithStreamingBehavior={agentRunning ? handlePromptWithStreamingBehavior : undefined}
|
||||||
|
scrollToBottomButton={
|
||||||
|
isEmptyNew ? null : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleScrollToConversationBottom}
|
||||||
|
aria-label={t("chat.scrollToBottom")}
|
||||||
|
title={t("chat.scrollToBottom")}
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
background: "var(--bg-panel)",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: atBottom ? 0.55 : 1,
|
||||||
|
transition: "opacity 0.15s, background 0.15s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="6 9 12 15 18 9" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
isStreaming={sessionBusy}
|
isStreaming={sessionBusy}
|
||||||
model={displayModelValue}
|
model={displayModelValue}
|
||||||
isAutoModelSelection={isAutoModelSelection}
|
isAutoModelSelection={isAutoModelSelection}
|
||||||
@@ -683,7 +740,9 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{streamState.isStreaming && streamState.streamingMessage && (
|
{streamState.isStreaming && streamState.streamingMessage && (
|
||||||
|
<div ref={streamBubbleRef}>
|
||||||
<MessageView message={streamState.streamingMessage as AgentMessage} isStreaming modelNames={modelNames} cwd={messageCwd} onOpenFile={onOpenFile} />
|
<MessageView message={streamState.streamingMessage as AgentMessage} isStreaming modelNames={modelNames} cwd={messageCwd} onOpenFile={onOpenFile} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{agentRunning && !streamState.streamingMessage && (
|
{agentRunning && !streamState.streamingMessage && (
|
||||||
@@ -710,10 +769,6 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{agentRunning && (
|
|
||||||
<div style={{ height: scrollContainerRef.current ? scrollContainerRef.current.clientHeight : "80vh" }} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ export type ThinkingLevelOption =
|
|||||||
|
|
||||||
const PROGRAMMATIC_SCROLL_IGNORE_MS = 700;
|
const PROGRAMMATIC_SCROLL_IGNORE_MS = 700;
|
||||||
const USER_SCROLL_INTENT_MS = 1200;
|
const USER_SCROLL_INTENT_MS = 1200;
|
||||||
|
// Distance from the container bottom (px) under which the view is
|
||||||
|
// considered "at the bottom". Auto-follow stays engaged within this range;
|
||||||
|
// scrolling further up disengages it.
|
||||||
|
const FOLLOW_STREAM_THRESHOLD = 100;
|
||||||
const PROMPT_SETTLE_INITIAL_DELAY_MS = 800;
|
const PROMPT_SETTLE_INITIAL_DELAY_MS = 800;
|
||||||
const PROMPT_SETTLE_POLL_MS = 600;
|
const PROMPT_SETTLE_POLL_MS = 600;
|
||||||
const PROMPT_SETTLE_MAX_MS = 20_000;
|
const PROMPT_SETTLE_MAX_MS = 20_000;
|
||||||
@@ -437,6 +441,7 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
isStreaming: false,
|
isStreaming: false,
|
||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
});
|
});
|
||||||
|
const [atBottom, setAtBottom] = useState(true);
|
||||||
const [agentRunning, setAgentRunning] = useState(false);
|
const [agentRunning, setAgentRunning] = useState(false);
|
||||||
const [bashRunning, setBashRunning] = useState(false);
|
const [bashRunning, setBashRunning] = useState(false);
|
||||||
const [pendingBash, setPendingBash] = useState<{
|
const [pendingBash, setPendingBash] = useState<{
|
||||||
@@ -523,6 +528,13 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
const lastUserMsgRef = useRef<HTMLDivElement | null>(null);
|
const lastUserMsgRef = useRef<HTMLDivElement | null>(null);
|
||||||
const pendingScrollToUserRef = useRef(false);
|
const pendingScrollToUserRef = useRef(false);
|
||||||
const completionScrollAllowedRef = useRef(true);
|
const completionScrollAllowedRef = useRef(true);
|
||||||
|
// Whether the view should auto-follow new content (streaming output,
|
||||||
|
// completion). True by default; a user scroll away from the bottom flips
|
||||||
|
// it off, scrolling back to the bottom or clicking the scroll-to-bottom
|
||||||
|
// button flips it on again. A ref (not state) because only the streaming
|
||||||
|
// chunk render needs to react to it.
|
||||||
|
const followStreamRef = useRef(true);
|
||||||
|
const atBottomRef = useRef(true);
|
||||||
const executeBashRef =
|
const executeBashRef =
|
||||||
useRef<
|
useRef<
|
||||||
(
|
(
|
||||||
@@ -970,7 +982,7 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "setStatus":
|
case "setStatus": {
|
||||||
setExtensionStatuses((prev) => {
|
setExtensionStatuses((prev) => {
|
||||||
const rest = prev.filter((item) => item.key !== request.statusKey);
|
const rest = prev.filter((item) => item.key !== request.statusKey);
|
||||||
return request.statusText !== undefined
|
return request.statusText !== undefined
|
||||||
@@ -978,7 +990,8 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
: rest;
|
: rest;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "setWidget":
|
}
|
||||||
|
case "setWidget": {
|
||||||
setExtensionWidgets((prev) => {
|
setExtensionWidgets((prev) => {
|
||||||
const rest = prev.filter((item) => item.key !== request.widgetKey);
|
const rest = prev.filter((item) => item.key !== request.widgetKey);
|
||||||
return request.widgetLines
|
return request.widgetLines
|
||||||
@@ -993,6 +1006,7 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
: rest;
|
: rest;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "setTitle":
|
case "setTitle":
|
||||||
if (request.title) document.title = request.title;
|
if (request.title) document.title = request.title;
|
||||||
break;
|
break;
|
||||||
@@ -1431,6 +1445,7 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
dispatch({ type: "start" });
|
dispatch({ type: "start" });
|
||||||
pendingScrollToUserRef.current = true;
|
pendingScrollToUserRef.current = true;
|
||||||
completionScrollAllowedRef.current = true;
|
completionScrollAllowedRef.current = true;
|
||||||
|
followStreamRef.current = true;
|
||||||
|
|
||||||
const piImages = images?.map((img) => ({
|
const piImages = images?.map((img) => ({
|
||||||
type: "image" as const,
|
type: "image" as const,
|
||||||
@@ -1995,6 +2010,7 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
||||||
|
followStreamRef.current = true;
|
||||||
ignoreProgrammaticScrollUntilRef.current =
|
ignoreProgrammaticScrollUntilRef.current =
|
||||||
Date.now() + PROGRAMMATIC_SCROLL_IGNORE_MS;
|
Date.now() + PROGRAMMATIC_SCROLL_IGNORE_MS;
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior });
|
messagesEndRef.current?.scrollIntoView({ behavior });
|
||||||
@@ -2026,10 +2042,36 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleScrollPositionChange = useCallback(() => {
|
const handleScrollPositionChange = useCallback(() => {
|
||||||
if (!agentRunningRef.current) return;
|
const container = scrollContainerRef.current;
|
||||||
if (Date.now() < ignoreProgrammaticScrollUntilRef.current) return;
|
if (!container) return;
|
||||||
|
const distFromBottom =
|
||||||
|
container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||||
|
const nearBottom = distFromBottom <= FOLLOW_STREAM_THRESHOLD;
|
||||||
|
|
||||||
|
// Keep the "at bottom" indicator (scroll-to-bottom button visibility)
|
||||||
|
// in sync on every scroll, regardless of agent state.
|
||||||
|
if (atBottomRef.current !== nearBottom) {
|
||||||
|
atBottomRef.current = nearBottom;
|
||||||
|
setAtBottom(nearBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-follow only reacts to genuine scroll gestures (wheel / touch /
|
||||||
|
// keyboard scroll keys inside the intent window). Programmatic scrolls
|
||||||
|
// must never flip it — right after sending, scrollUserMsgToTop parks
|
||||||
|
// the view at the user message (away from the bottom because of the
|
||||||
|
// running spacer); treating that as a user scroll-away would disable
|
||||||
|
// follow before the stream even starts. A plain click (send button,
|
||||||
|
// minimap) is also not a scroll gesture.
|
||||||
if (Date.now() > userScrollIntentUntilRef.current) return;
|
if (Date.now() > userScrollIntentUntilRef.current) return;
|
||||||
|
|
||||||
|
// Scrolled away from the bottom → stop following the stream; scrolled
|
||||||
|
// back to the bottom → resume following.
|
||||||
|
if (followStreamRef.current !== nearBottom) {
|
||||||
|
followStreamRef.current = nearBottom;
|
||||||
|
if (!nearBottom && agentRunningRef.current) {
|
||||||
completionScrollAllowedRef.current = false;
|
completionScrollAllowedRef.current = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load session on mount
|
// Load session on mount
|
||||||
@@ -2105,13 +2147,16 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
}, [data?.tree, activeLeafId, handleLeafChange, onBranchDataChange]);
|
}, [data?.tree, activeLeafId, handleLeafChange, onBranchDataChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Only genuine scroll gestures (wheel / touch / keyboard scroll keys)
|
||||||
|
// count as user scroll intent. A plain pointerdown (clicking send, a
|
||||||
|
// button, the minimap) is NOT — otherwise the programmatic
|
||||||
|
// scrollUserMsgToTop that runs right after sending would be treated as
|
||||||
|
// user-initiated, the handler would see the view away from the bottom
|
||||||
|
// (the user message sits above the spacer), and auto-follow would be
|
||||||
|
// disabled before the stream even starts.
|
||||||
window.addEventListener("keydown", markUserScrollIntent);
|
window.addEventListener("keydown", markUserScrollIntent);
|
||||||
window.addEventListener("pointerdown", markUserScrollIntent, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("keydown", markUserScrollIntent);
|
window.removeEventListener("keydown", markUserScrollIntent);
|
||||||
window.removeEventListener("pointerdown", markUserScrollIntent);
|
|
||||||
};
|
};
|
||||||
}, [markUserScrollIntent]);
|
}, [markUserScrollIntent]);
|
||||||
|
|
||||||
@@ -2247,6 +2292,9 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
|
|||||||
lastUserMsgRef,
|
lastUserMsgRef,
|
||||||
pendingScrollToUserRef,
|
pendingScrollToUserRef,
|
||||||
initialScrollDoneRef,
|
initialScrollDoneRef,
|
||||||
|
followStreamRef,
|
||||||
|
atBottom,
|
||||||
|
scrollToBottom,
|
||||||
// Actions
|
// Actions
|
||||||
handleSend,
|
handleSend,
|
||||||
handleAbort,
|
handleAbort,
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export const enLocale: LocalePlugin = {
|
|||||||
"chat.commandCopy": "Copy the last assistant message",
|
"chat.commandCopy": "Copy the last assistant message",
|
||||||
"chat.compacted": "Compacted",
|
"chat.compacted": "Compacted",
|
||||||
"chat.tokensSaved": "{saved} saved",
|
"chat.tokensSaved": "{saved} saved",
|
||||||
|
"chat.scrollToBottom": "Scroll to bottom",
|
||||||
"mcp.addServer": "Add server",
|
"mcp.addServer": "Add server",
|
||||||
"mcp.addEnv": "Add env",
|
"mcp.addEnv": "Add env",
|
||||||
"mcp.serverName": "Server name",
|
"mcp.serverName": "Server name",
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export const zhCNLocale: LocalePlugin = {
|
|||||||
"chat.commandCopy": "复制最后一条助手消息",
|
"chat.commandCopy": "复制最后一条助手消息",
|
||||||
"chat.compacted": "已压缩",
|
"chat.compacted": "已压缩",
|
||||||
"chat.tokensSaved": "节省 {saved}",
|
"chat.tokensSaved": "节省 {saved}",
|
||||||
|
"chat.scrollToBottom": "滚动到底部",
|
||||||
"mcp.addServer": "添加服务器",
|
"mcp.addServer": "添加服务器",
|
||||||
"mcp.addEnv": "添加环境变量",
|
"mcp.addEnv": "添加环境变量",
|
||||||
"mcp.serverName": "服务器名称",
|
"mcp.serverName": "服务器名称",
|
||||||
|
|||||||
Reference in New Issue
Block a user