): void => {
+ const container = scrollContainerRef.current;
+ if (!container) return;
+ const step = Math.max(1, container.clientHeight * 0.8);
+ if (event.key === "Home") container.scrollTo({ top: 0, behavior: "auto" });
+ else if (event.key === "End") container.scrollTo({ top: container.scrollHeight, behavior: "auto" });
+ else if (event.key === "PageUp") container.scrollTo({ top: container.scrollTop - step, behavior: "auto" });
+ else if (event.key === "PageDown") container.scrollTo({ top: container.scrollTop + step, behavior: "auto" });
+ else return;
+ event.preventDefault();
+ };
+
+ return (
+ { dragRef.current = undefined; }}
+ onPointerCancel={endDrag}
+ onPointerDown={onPointerDown}
+ onPointerEnter={() => setExpanded(true)}
+ onPointerLeave={() => { if (!dragRef.current) setExpanded(false); }}
+ onPointerMove={onPointerMove}
+ onPointerUp={(event) => {
+ endDrag(event);
+ const bounds = railRef.current?.getBoundingClientRect();
+ if (bounds && (event.clientX < bounds.left || event.clientX > bounds.right)) setExpanded(false);
+ }}
+ onWheel={onWheel}
+ ref={railRef}
+ // Shown by sync() once the scroller overflows; React never rewrites it.
+ style={{ display: "none" }}
+ role="scrollbar"
+ tabIndex={0}
+ >
+
+
+
+ );
+}
diff --git a/src/renderer/src/ConversationHistorySlot.tsx b/src/renderer/src/ConversationHistorySlot.tsx
index fd0ec5ca..38ab2d97 100644
--- a/src/renderer/src/ConversationHistorySlot.tsx
+++ b/src/renderer/src/ConversationHistorySlot.tsx
@@ -37,7 +37,9 @@ function ConversationHistoryLoader({ conversationId, active, load }: {
;
}
-type PaneProps = Omit, "conversation" | "taskStrip" | "artifactById">;
+type PaneProps = Omit, "conversation" | "taskStrip" | "artifactById" | "onVisibleMessageCountChange" | "visibleMessageCount">;
+
+const ignoreVisibleMessageCountChange = (): void => undefined;
export type ConversationHistorySlotProps = PaneProps & {
conversationId: string;
@@ -110,5 +112,12 @@ export const ConversationHistorySlot = memo(function ConversationHistorySlot({
/>
);
}
- return ;
+ return ;
});
diff --git a/src/renderer/src/i18n/locales/en-US/app.ts b/src/renderer/src/i18n/locales/en-US/app.ts
index 7313652d..f9adad9a 100644
--- a/src/renderer/src/i18n/locales/en-US/app.ts
+++ b/src/renderer/src/i18n/locales/en-US/app.ts
@@ -439,6 +439,7 @@ export const app = {
copyMessage: 'Copy this response',
loadEarlierMessages: 'Load earlier messages ({{count}} remaining)',
scrollToBottom: 'Scroll to bottom',
+ minimap: 'Conversation minimap',
status: {
preparingRequest: 'Preparing request',
waitingForProgress: 'Waiting for further progress',
diff --git a/src/renderer/src/i18n/locales/zh-CN/app.ts b/src/renderer/src/i18n/locales/zh-CN/app.ts
index a2ac91b4..d5f6a54b 100644
--- a/src/renderer/src/i18n/locales/zh-CN/app.ts
+++ b/src/renderer/src/i18n/locales/zh-CN/app.ts
@@ -424,6 +424,7 @@ export const app = {
copyMessage: '复制此回复',
loadEarlierMessages: '加载更早的消息(还剩 {{count}} 条)',
scrollToBottom: '到底部',
+ minimap: '对话缩略图',
status: {
preparingRequest: '正在准备请求',
waitingForProgress: '等待后续进展',
diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css
index 86abb23d..2e16d515 100644
--- a/src/renderer/src/styles.css
+++ b/src/renderer/src/styles.css
@@ -7343,6 +7343,84 @@ button > svg {
color: var(--danger);
}
+/* The minimap replaces the native scrollbar of the message scroller. */
+.chat-history-pane > .chat {
+ scrollbar-width: none;
+}
+
+.chat-history-pane > .chat::-webkit-scrollbar {
+ display: none;
+}
+
+/* The rail covers only the reading area: below the context strip and, with
+ frosted glass, below the title bar the pane extends under. */
+.chat-minimap {
+ position: absolute;
+ z-index: 3;
+ top: var(--chat-context-height, 0px);
+ right: 0;
+ bottom: 0;
+ width: 14px;
+ overflow: hidden;
+ background: transparent;
+ cursor: default;
+ touch-action: none;
+ user-select: none;
+}
+
+.app-shell[data-frosted-glass='true'] .chat-minimap {
+ top: calc(58px + var(--chat-context-height, 0px));
+}
+
+.chat-minimap[data-expanded='true'] {
+ width: 80px;
+ border-left: 1px solid var(--border-default);
+ background: var(--surface-glass-panel);
+ box-shadow: var(--shadow-card);
+ backdrop-filter: blur(8px);
+}
+
+.chat-minimap:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: -2px;
+}
+
+.chat-minimap__canvas {
+ display: block;
+ width: 100%;
+ height: 100%;
+ opacity: 0.55;
+}
+
+.chat-minimap[data-expanded='true'] .chat-minimap__canvas {
+ opacity: 0.85;
+}
+
+.chat-minimap__viewport {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ background: color-mix(in srgb, var(--text-muted, #8f98a8) 22%, transparent);
+ pointer-events: none;
+ will-change: transform;
+}
+
+.chat-minimap:hover .chat-minimap__viewport,
+.chat-minimap:active .chat-minimap__viewport {
+ background: color-mix(in srgb, var(--accent) 24%, transparent);
+}
+
+@media (max-width: 700px) {
+ .chat-minimap {
+ width: 10px;
+ }
+
+ .chat-minimap[data-expanded='true'] {
+ width: 64px;
+ }
+}
+
.message__status--active {
color: var(--text-muted);
}