Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f76712c
Add AI-powered automatic tab title generation
stevenvo Dec 5, 2025
e08c0cb
Add drag-and-drop file support to terminal
stevenvo Dec 5, 2025
85858e1
Fix drag-and-drop to use full file paths
stevenvo Dec 5, 2025
da2595c
Add Electron API for getting file paths in drag-drop
stevenvo Dec 5, 2025
7b73a2d
Fix: Import getApi in terminal component for drag-drop
stevenvo Dec 5, 2025
da20c87
Replace AI tab titles with folder-based naming and UX improvements
stevenvo Dec 6, 2025
da450d0
Add UX improvements: scroll preservation, close confirmation, and mar…
stevenvo Dec 8, 2025
f6e27d2
Make file browser default to terminal's current working directory
stevenvo Dec 8, 2025
0709c3d
Add light theme and make file browser follow terminal cwd
stevenvo Dec 8, 2025
ec04394
Use GitHub light theme for markdown code syntax highlighting
stevenvo Dec 8, 2025
06b2038
Revert "Use GitHub light theme for markdown code syntax highlighting"
stevenvo Dec 8, 2025
cf1c511
Revert "Add light theme and make file browser follow terminal cwd"
stevenvo Dec 8, 2025
048b7a1
Add White background preset option
stevenvo Dec 8, 2025
d59fe56
Add macOS Basic light terminal theme
stevenvo Dec 8, 2025
e4164c4
Change default terminal font to SF Mono 11pt like macOS Terminal
stevenvo Dec 8, 2025
6339407
Use terminal theme background instead of transparent overlay
stevenvo Dec 8, 2025
cde3b7b
Adjust font rendering to match macOS Terminal appearance
stevenvo Dec 8, 2025
58fbaac
Increase bold font weight to 700 for better visibility
stevenvo Dec 8, 2025
855788d
Revert "Increase bold font weight to 700 for better visibility"
stevenvo Dec 8, 2025
c3be1cf
Revert "Adjust font rendering to match macOS Terminal appearance"
stevenvo Dec 8, 2025
7e25cb6
Revert "Use terminal theme background instead of transparent overlay"
stevenvo Dec 8, 2025
e4d2df6
Revert "Change default terminal font to SF Mono 11pt like macOS Termi…
stevenvo Dec 8, 2025
7d9a0a5
Revert "Add macOS Basic light terminal theme"
stevenvo Dec 8, 2025
56e7c17
Revert "Add White background preset option"
stevenvo Dec 8, 2025
069fa4c
Preserve terminal scroll position during all writes
stevenvo Dec 10, 2025
a378f7b
Revert "Preserve terminal scroll position during all writes"
stevenvo Dec 10, 2025
fe684c9
Add Cmd+Up/Down hotkeys to scroll terminal to top/bottom
stevenvo Dec 12, 2025
6c3404f
Hide WaveAI button from top menu bar
stevenvo Dec 12, 2025
def8d03
Update generated RPC client files for tab title feature
stevenvo Dec 12, 2025
c570bf3
Revert "Hide WaveAI button from top menu bar"
stevenvo Dec 13, 2025
4970ac4
Reapply "Hide WaveAI button from top menu bar"
stevenvo Dec 13, 2025
18ab50f
Address CodeRabbit review feedback
stevenvo Dec 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions emain/emain-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ import path from "path";
import { WaveDevVarName, WaveDevViteVarName } from "../frontend/util/isdev";
import * as keyutil from "../frontend/util/keyutil";

// This is a little trick to ensure that Electron puts all its runtime data into a subdirectory to avoid conflicts with our own data.
// On macOS, it will store to ~/Library/Application \Support/waveterm/electron
// On Linux, it will store to ~/.config/waveterm/electron
// On Windows, it will store to %LOCALAPPDATA%/waveterm/electron
app.setName("waveterm/electron");

const isDev = !app.isPackaged;
const isDevVite = isDev && process.env.ELECTRON_RENDERER_URL;
console.log(`Running in ${isDev ? "development" : "production"} mode`);
Expand All @@ -32,7 +26,12 @@ const waveDirName = `${waveDirNamePrefix}${waveDirNameSuffix ? `-${waveDirNameSu

const paths = envPaths("waveterm", { suffix: waveDirNameSuffix });

// Set the proper display name first
app.setName(isDev ? "Wave (Dev)" : "Wave");

// Note: We previously used app.setName("waveterm/electron") here to organize Electron's runtime data,
// but this caused "Electron" to appear in the macOS menu bar. The envPaths configuration above
// already handles the data directory organization, so the setName trick is not needed.
const unamePlatform = process.platform;
const unameArch: string = process.arch;
keyutil.setKeyUtilPlatform(unamePlatform);
Expand Down
3 changes: 2 additions & 1 deletion emain/preload.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { contextBridge, ipcRenderer, Rectangle, WebviewTag } from "electron";
import { contextBridge, ipcRenderer, Rectangle, webUtils, WebviewTag } from "electron";

// update type in custom.d.ts (ElectronApi type)
contextBridge.exposeInMainWorld("api", {
Expand Down Expand Up @@ -68,6 +68,7 @@ contextBridge.exposeInMainWorld("api", {
openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId),
setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId),
doRefresh: () => ipcRenderer.send("do-refresh"),
getPathForFile: (file: File) => webUtils.getPathForFile(file),
});

// Custom event for "new-window"
Expand Down
43 changes: 40 additions & 3 deletions frontend/app/aipanel/aipanelmessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { AIModeDropdown } from "./aimode";
import { type WaveUIMessage } from "./aitypes";
import { WaveAIModel } from "./waveai-model";

const AUTO_SCROLL_DEBOUNCE_MS = 100;
const SCROLL_BOTTOM_THRESHOLD_PX = 50;

interface AIPanelMessagesProps {
messages: WaveUIMessage[];
status: string;
Expand All @@ -20,25 +23,59 @@ export const AIPanelMessages = memo(({ messages, status, onContextMenu }: AIPane
const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const prevStatusRef = useRef<string>(status);
const userHasScrolledUp = useRef<boolean>(false);
const isAutoScrolling = useRef<boolean>(false);

const scrollToBottom = () => {
const container = messagesContainerRef.current;
if (container) {
isAutoScrolling.current = true;
container.scrollTop = container.scrollHeight;
container.scrollLeft = 0;
userHasScrolledUp.current = false;
setTimeout(() => {
isAutoScrolling.current = false;
}, AUTO_SCROLL_DEBOUNCE_MS);
}
};

// Detect if user has manually scrolled up
useEffect(() => {
const container = messagesContainerRef.current;
if (!container) return;

const handleScroll = () => {
// Ignore scroll events triggered by our auto-scroll
if (isAutoScrolling.current) return;

const { scrollTop, scrollHeight, clientHeight } = container;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;

// If user is more than threshold from the bottom, they've scrolled up
if (distanceFromBottom > SCROLL_BOTTOM_THRESHOLD_PX) {
userHasScrolledUp.current = true;
} else {
userHasScrolledUp.current = false;
}
};

container.addEventListener("scroll", handleScroll, { passive: true });
return () => container.removeEventListener("scroll", handleScroll);
}, []);

useEffect(() => {
model.registerScrollToBottom(scrollToBottom);
}, [model]);

useEffect(() => {
scrollToBottom();
// Only auto-scroll if user hasn't manually scrolled up
if (!userHasScrolledUp.current) {
scrollToBottom();
}
}, [messages]);
Comment on lines 70 to 75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find the file and check its existence
fd -t f "aipanelmessages.tsx" --type f

# Check how large it is
wc -l frontend/app/aipanel/aipanelmessages.tsx 2>/dev/null || echo "File not found, searching..."

# Search for userHasScrolledUp usage
rg -n "userHasScrolledUp" -t tsx -t ts 2>/dev/null || rg -n "userHasScrolledUp"

Repository: wavetermdev/waveterm

Length of output: 837


🏁 Script executed:

#!/bin/bash
# Search for conversation reset or message clearing logic
rg -n "messages.*\[\]|clearMessages|resetConversation|conversation.*reset" -t tsx -t ts 2>/dev/null || rg -n "messages.*\[\]|clearMessages|resetConversation|conversation.*reset"

Repository: wavetermdev/waveterm

Length of output: 3173


🏁 Script executed:

#!/bin/bash
# Read the full file to understand the complete context
cat -n frontend/app/aipanel/aipanelmessages.tsx

Repository: wavetermdev/waveterm

Length of output: 5151


🏁 Script executed:

#!/bin/bash
# Check the parent component to see how messages are managed and if there's any reset logic
rg -n "AIPanelMessages" -t tsx -t ts -B 5 -A 5 | head -100

Repository: wavetermdev/waveterm

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Look for the model.clearMessages or conversation reset in waveai-model.tsx
sed -n '370,390p' frontend/app/aipanel/waveai-model.tsx

Repository: wavetermdev/waveterm

Length of output: 1039


🏁 Script executed:

#!/bin/bash
# Check if there's any reset logic in parent or if messages completely empty means new conversation
rg -n "clearChat|newChat|clearMessages" frontend/app/aipanel/ -A 3

Repository: wavetermdev/waveterm

Length of output: 2001


Edge case: userHasScrolledUp persists across conversation resets.

When a user scrolls up in one conversation, then clears the chat or starts a new conversation (via clearChat() which calls useChatSetMessages([]) and resets the messages array), the userHasScrolledUp flag remains true. This prevents auto-scrolling to new messages in the fresh conversation since the effect at lines 67-72 only calls scrollToBottom() when !userHasScrolledUp.current. The flag only resets when the user manually scrolls near the bottom or scrollToBottom() is explicitly called. Consider resetting this flag when the conversation changes—for instance, by detecting when messages.length === 0 within the messages effect, or by adding an explicit reset trigger.

🤖 Prompt for AI Agents
In frontend/app/aipanel/aipanelmessages.tsx around lines 67 to 72, the
userHasScrolledUp ref persists across conversation resets so auto-scroll is
blocked for new/cleared conversations; update the messages effect to detect a
conversation reset (e.g., messages.length === 0 or a conversationId change) and
reset userHasScrolledUp.current = false (or call scrollToBottom()) when a
new/empty conversation is detected so subsequent incoming messages will
auto-scroll as expected.


useEffect(() => {
if (isPanelOpen) {
if (isPanelOpen && !userHasScrolledUp.current) {
scrollToBottom();
}
}, [isPanelOpen]);
Expand All @@ -47,7 +84,7 @@ export const AIPanelMessages = memo(({ messages, status, onContextMenu }: AIPane
const wasStreaming = prevStatusRef.current === "streaming";
const isNowNotStreaming = status !== "streaming";

if (wasStreaming && isNowNotStreaming) {
if (wasStreaming && isNowNotStreaming && !userHasScrolledUp.current) {
requestAnimationFrame(() => {
scrollToBottom();
});
Expand Down
39 changes: 39 additions & 0 deletions frontend/app/modals/confirmclosetab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { Modal } from "@/app/modals/modal";
import { deleteLayoutModelForTab } from "@/layout/index";
import { atoms, getApi, globalStore } from "@/store/global";
import { modalsModel } from "@/store/modalmodel";

interface ConfirmCloseTabModalProps {
tabId: string;
}

const ConfirmCloseTabModal = ({ tabId }: ConfirmCloseTabModalProps) => {
const handleConfirmClose = () => {
const ws = globalStore.get(atoms.workspace);
getApi().closeTab(ws.oid, tabId);
deleteLayoutModelForTab(tabId);
modalsModel.popModal();
};
Comment on lines +14 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling and await async operations.

The closeTab API call is likely asynchronous but isn't awaited, causing the modal to close immediately regardless of whether the operation succeeds. If the API call fails, the user won't receive feedback.

Additionally, there's no validation that the workspace exists or that the tabId is valid.

Consider this refactor:

-    const handleConfirmClose = () => {
+    const handleConfirmClose = async () => {
+        try {
             const ws = globalStore.get(atoms.workspace);
-            getApi().closeTab(ws.oid, tabId);
+            if (!ws?.oid || !tabId) {
+                console.error("Invalid workspace or tabId");
+                modalsModel.popModal();
+                return;
+            }
+            await getApi().closeTab(ws.oid, tabId);
             deleteLayoutModelForTab(tabId);
             modalsModel.popModal();
+        } catch (error) {
+            console.error("Failed to close tab:", error);
+            // Consider showing an error message to the user
+            modalsModel.popModal();
+        }
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleConfirmClose = () => {
const ws = globalStore.get(atoms.workspace);
getApi().closeTab(ws.oid, tabId);
deleteLayoutModelForTab(tabId);
modalsModel.popModal();
};
const handleConfirmClose = async () => {
try {
const ws = globalStore.get(atoms.workspace);
if (!ws?.oid || !tabId) {
console.error("Invalid workspace or tabId");
modalsModel.popModal();
return;
}
await getApi().closeTab(ws.oid, tabId);
deleteLayoutModelForTab(tabId);
modalsModel.popModal();
} catch (error) {
console.error("Failed to close tab:", error);
// Consider showing an error message to the user
modalsModel.popModal();
}
};
🤖 Prompt for AI Agents
In frontend/app/modals/confirmclosetab.tsx around lines 14 to 19, the code calls
getApi().closeTab(...) without awaiting it, lacks validation for workspace and
tabId, and provides no error feedback; update handleConfirmClose to: validate
that globalStore.get(atoms.workspace) exists and that tabId is present, set a
local loading state, await the async closeTab call inside a try/catch, only call
deleteLayoutModelForTab(tabId) and modalsModel.popModal() on success, and in the
catch block surface the error to the user (e.g., set an error state or call a
notification logger); ensure loading is cleared in finally so the UI reflects
operation progress.


const handleCancel = () => {
modalsModel.popModal();
};

return (
<Modal onOk={handleConfirmClose} onCancel={handleCancel} onClose={handleCancel}>
<div className="content">
<div className="modal-title">Close Tab?</div>
<div style={{ marginTop: "10px" }}>
Are you sure you want to close this tab? This action cannot be undone.
</div>
</div>
</Modal>
);
};

ConfirmCloseTabModal.displayName = "ConfirmCloseTabModal";

export { ConfirmCloseTabModal };
2 changes: 2 additions & 0 deletions frontend/app/modals/modalregistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade";
import { DeleteFileModal, PublishAppModal, RenameFileModal } from "@/builder/builder-apppanel";
import { SetSecretDialog } from "@/builder/tabs/builder-secrettab";
import { AboutModal } from "./about";
import { ConfirmCloseTabModal } from "./confirmclosetab";
import { UserInputModal } from "./userinputmodal";

const modalRegistry: { [key: string]: React.ComponentType<any> } = {
Expand All @@ -15,6 +16,7 @@ const modalRegistry: { [key: string]: React.ComponentType<any> } = {
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
[AboutModal.displayName || "AboutModal"]: AboutModal,
[MessageModal.displayName || "MessageModal"]: MessageModal,
[ConfirmCloseTabModal.displayName || "ConfirmCloseTabModal"]: ConfirmCloseTabModal,
[PublishAppModal.displayName || "PublishAppModal"]: PublishAppModal,
[RenameFileModal.displayName || "RenameFileModal"]: RenameFileModal,
[DeleteFileModal.displayName || "DeleteFileModal"]: DeleteFileModal,
Expand Down
5 changes: 5 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ class RpcApiType {
return client.wshRpcCall("focuswindow", data, opts);
}

// command "generatetabtitle" [call]
GenerateTabTitleCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<string> {
return client.wshRpcCall("generatetabtitle", data, opts);
}

// command "getbuilderoutput" [call]
GetBuilderOutputCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<string[]> {
return client.wshRpcCall("getbuilderoutput", data, opts);
Expand Down
8 changes: 3 additions & 5 deletions frontend/app/tab/tabbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -620,10 +620,9 @@ const TabBar = memo(({ workspace }: TabBarProps) => {

const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, tabId: string) => {
event?.stopPropagation();
const ws = globalStore.get(atoms.workspace);
getApi().closeTab(ws.oid, tabId);
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease");
deleteLayoutModelForTab(tabId);

// Show confirmation modal before closing
modalsModel.pushModal("ConfirmCloseTabModal", { tabId });
};

const handlePinChange = useCallback(
Expand Down Expand Up @@ -698,7 +697,6 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
<i className="fa fa-ellipsis" />
</div>
)}
<WaveAIButton />
<WorkspaceSwitcher ref={workspaceSwitcherRef} />
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/view/preview/preview-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@ function CodeEditPreview({ model }: SpecializedViewProps) {

useEffect(() => {
model.codeEditKeyDownHandler = codeEditKeyDownHandler;
model.refreshCallback = () => {
globalStore.set(model.refreshVersion, (v) => v + 1);
};
return () => {
model.codeEditKeyDownHandler = null;
model.monacoRef.current = null;
model.refreshCallback = null;
};
}, []);

Expand Down
12 changes: 10 additions & 2 deletions frontend/app/view/preview/preview-markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@
// SPDX-License-Identifier: Apache-2.0

import { Markdown } from "@/element/markdown";
import { getOverrideConfigAtom } from "@/store/global";
import { getOverrideConfigAtom, globalStore } from "@/store/global";
import { useAtomValue } from "jotai";
import { useMemo } from "react";
import { useEffect, useMemo } from "react";
import type { SpecializedViewProps } from "./preview";

function MarkdownPreview({ model }: SpecializedViewProps) {
useEffect(() => {
model.refreshCallback = () => {
globalStore.set(model.refreshVersion, (v) => v + 1);
};
return () => {
model.refreshCallback = null;
};
}, []);
const connName = useAtomValue(model.connection);
const fileInfo = useAtomValue(model.statFile);
const fontSizeOverride = useAtomValue(getOverrideConfigAtom(model.blockId, "markdown:fontsize"));
Expand Down
25 changes: 24 additions & 1 deletion frontend/app/view/preview/preview-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,13 +350,35 @@ export class PreviewModel implements ViewModel {
title: "Table of Contents",
click: () => this.markdownShowTocToggle(),
},
{
elemtype: "iconbutton",
icon: "arrows-rotate",
title: "Refresh",
click: () => this.refreshCallback?.(),
},
] as IconButtonDecl[];
} else if (!isCeView && mimeType) {
// For all other file types (text, code, etc.), add refresh button
return [
{
elemtype: "iconbutton",
icon: "arrows-rotate",
title: "Refresh",
click: () => this.refreshCallback?.(),
},
] as IconButtonDecl[];
Comment on lines +353 to 369
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "refreshCallback\s*=" frontend/app/view/preview/

Repository: wavetermdev/waveterm

Length of output: 789


🏁 Script executed:

rg -n "refreshCallback" frontend/app/view/preview/ -A 3 -B 3

Repository: wavetermdev/waveterm

Length of output: 9670


🏁 Script executed:

fd . frontend/app/view/preview -name "*.tsx" -o -name "*.ts" | head -20

Repository: wavetermdev/waveterm

Length of output: 294


Fix unsafe refreshCallback invocations in preview-directory operations.

The refresh buttons correctly use optional chaining refreshCallback?.(), but internal directory operations directly invoke model.refreshCallback() without null safety. In preview-directory-utils.tsx (lines 132, 173) and preview-directory.tsx (lines 758, 818, 835), these direct calls will throw if the callback is null. While all specialized views initialize the callback in their useEffect hooks, the direct invocations should also use optional chaining to match the defensive pattern established in the button handlers.

🤖 Prompt for AI Agents
In frontend/app/view/preview/preview-model.tsx around lines 353-369, and in
preview-directory-utils.tsx (around lines 132 and 173) and preview-directory.tsx
(around lines 758, 818, 835), direct invocations of refreshCallback are unsafe
(they call model.refreshCallback() or this.refreshCallback() without null
checks); change those direct calls to use optional chaining (e.g.,
model.refreshCallback?.() or this.refreshCallback?.()) so the callback is only
invoked when present, matching the defensive pattern used in the icon button
handlers.

}
return null;
});
this.metaFilePath = atom<string>((get) => {
const file = get(this.blockAtom)?.meta?.file;
const blockData = get(this.blockAtom);
const file = blockData?.meta?.file;
if (isBlank(file)) {
// If no file is set, default to the terminal's current working directory
const cwd = blockData?.meta?.[waveobj.MetaKey_CmdCwd];
if (!isBlank(cwd)) {
return cwd;
}
return "~";
}
return file;
Expand Down Expand Up @@ -408,6 +430,7 @@ export class PreviewModel implements ViewModel {
this.goParentDirectory = this.goParentDirectory.bind(this);

const fullFileAtom = atom<Promise<FileData>>(async (get) => {
get(this.refreshVersion); // Subscribe to refreshVersion to trigger re-fetch
const fileName = get(this.metaFilePath);
const path = await this.formatRemoteUri(fileName, get);
if (fileName == null) {
Expand Down
10 changes: 10 additions & 0 deletions frontend/app/view/preview/preview-streaming.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

import { Button } from "@/app/element/button";
import { CenteredDiv } from "@/app/element/quickelems";
import { globalStore } from "@/store/global";
import { getWebServerEndpoint } from "@/util/endpoints";
import { formatRemoteUri } from "@/util/waveutil";
import { useAtomValue } from "jotai";
import { useEffect } from "react";
import { TransformComponent, TransformWrapper, useControls } from "react-zoom-pan-pinch";
import type { SpecializedViewProps } from "./preview";

Expand Down Expand Up @@ -45,6 +47,14 @@ function StreamingImagePreview({ url }: { url: string }) {
}

function StreamingPreview({ model }: SpecializedViewProps) {
useEffect(() => {
model.refreshCallback = () => {
globalStore.set(model.refreshVersion, (v) => v + 1);
};
return () => {
model.refreshCallback = null;
};
}, []);
const conn = useAtomValue(model.connection);
const fileInfo = useAtomValue(model.statFile);
const filePath = fileInfo.path;
Expand Down
14 changes: 14 additions & 0 deletions frontend/app/view/term/term-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,20 @@ export class TermViewModel implements ViewModel {
this.setTermMode(newTermMode);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowDown")) {
// Scroll to bottom
if (this.termRef?.current?.terminal) {
this.termRef.current.terminal.scrollToBottom();
}
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowUp")) {
// Scroll to top
if (this.termRef?.current?.terminal) {
this.termRef.current.terminal.scrollToLine(0);
}
return true;
}
const blockData = globalStore.get(this.blockAtom);
if (blockData.meta?.["term:mode"] == "vdom") {
const vdomModel = this.getVDomModel();
Expand Down
Loading