diff --git a/emain/emain-platform.ts b/emain/emain-platform.ts index 32320e4eb..bf4f8cbb4 100644 --- a/emain/emain-platform.ts +++ b/emain/emain-platform.ts @@ -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`); @@ -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); diff --git a/emain/preload.ts b/emain/preload.ts index c6bdf1498..ee60c1360 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -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", { @@ -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" diff --git a/frontend/app/aipanel/aipanelmessages.tsx b/frontend/app/aipanel/aipanelmessages.tsx index a0284153d..3920197cc 100644 --- a/frontend/app/aipanel/aipanelmessages.tsx +++ b/frontend/app/aipanel/aipanelmessages.tsx @@ -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; @@ -20,25 +23,59 @@ export const AIPanelMessages = memo(({ messages, status, onContextMenu }: AIPane const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const prevStatusRef = useRef(status); + const userHasScrolledUp = useRef(false); + const isAutoScrolling = useRef(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]); useEffect(() => { - if (isPanelOpen) { + if (isPanelOpen && !userHasScrolledUp.current) { scrollToBottom(); } }, [isPanelOpen]); @@ -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(); }); diff --git a/frontend/app/modals/confirmclosetab.tsx b/frontend/app/modals/confirmclosetab.tsx new file mode 100644 index 000000000..a431fcb45 --- /dev/null +++ b/frontend/app/modals/confirmclosetab.tsx @@ -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(); + }; + + const handleCancel = () => { + modalsModel.popModal(); + }; + + return ( + +
+
Close Tab?
+
+ Are you sure you want to close this tab? This action cannot be undone. +
+
+
+ ); +}; + +ConfirmCloseTabModal.displayName = "ConfirmCloseTabModal"; + +export { ConfirmCloseTabModal }; diff --git a/frontend/app/modals/modalregistry.tsx b/frontend/app/modals/modalregistry.tsx index 53fabde06..a19cb4704 100644 --- a/frontend/app/modals/modalregistry.tsx +++ b/frontend/app/modals/modalregistry.tsx @@ -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 } = { @@ -15,6 +16,7 @@ const modalRegistry: { [key: string]: React.ComponentType } = { [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, diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 59945312d..28a6d332d 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -292,6 +292,11 @@ class RpcApiType { return client.wshRpcCall("focuswindow", data, opts); } + // command "generatetabtitle" [call] + GenerateTabTitleCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + return client.wshRpcCall("generatetabtitle", data, opts); + } + // command "getbuilderoutput" [call] GetBuilderOutputCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { return client.wshRpcCall("getbuilderoutput", data, opts); diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 2a479aa63..aeb88ca52 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -620,10 +620,9 @@ const TabBar = memo(({ workspace }: TabBarProps) => { const handleCloseTab = (event: React.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( @@ -698,7 +697,6 @@ const TabBar = memo(({ workspace }: TabBarProps) => { )} -
diff --git a/frontend/app/view/preview/preview-edit.tsx b/frontend/app/view/preview/preview-edit.tsx index 3e79dbced..f2df68ba6 100644 --- a/frontend/app/view/preview/preview-edit.tsx +++ b/frontend/app/view/preview/preview-edit.tsx @@ -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; }; }, []); diff --git a/frontend/app/view/preview/preview-markdown.tsx b/frontend/app/view/preview/preview-markdown.tsx index 6eda00d3c..22bba8888 100644 --- a/frontend/app/view/preview/preview-markdown.tsx +++ b/frontend/app/view/preview/preview-markdown.tsx @@ -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")); diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index b6aa8313b..a9c4d277b 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -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[]; } return null; }); this.metaFilePath = atom((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; @@ -408,6 +430,7 @@ export class PreviewModel implements ViewModel { this.goParentDirectory = this.goParentDirectory.bind(this); const fullFileAtom = atom>(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) { diff --git a/frontend/app/view/preview/preview-streaming.tsx b/frontend/app/view/preview/preview-streaming.tsx index f16babe7f..d45a362c4 100644 --- a/frontend/app/view/preview/preview-streaming.tsx +++ b/frontend/app/view/preview/preview-streaming.tsx @@ -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"; @@ -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; diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index a1b9eb60d..72e45439e 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -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(); diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 387155752..8104cc7f7 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -7,7 +7,7 @@ import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import type { TermViewModel } from "@/app/view/term/term-model"; -import { atoms, getOverrideConfigAtom, getSettingsPrefixAtom, globalStore, WOS } from "@/store/global"; +import { atoms, getApi, getOverrideConfigAtom, getSettingsPrefixAtom, globalStore, WOS } from "@/store/global"; import { fireAndForget, useAtomValueSafe } from "@/util/util"; import { computeBgStyleFromMeta } from "@/util/waveutil"; import { ISearchOptions } from "@xterm/addon-search"; @@ -349,8 +349,78 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => const termBg = computeBgStyleFromMeta(blockData?.meta); + // Handle drag and drop + const handleDragOver = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + // Indicate that we can accept the drop + e.dataTransfer.dropEffect = "copy"; + }, []); + + const handleDrop = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Get files from the drop + const files = Array.from(e.dataTransfer.files); + if (files.length === 0) { + return; + } + + console.log("Drop files:", files); + + // Get the file path(s) using the Electron API + const paths = files.map((file: File) => { + try { + // Use the exposed Electron API to get the full path + const fullPath = getApi().getPathForFile(file); + console.log("File:", file.name, "-> Full path:", fullPath); + return fullPath; + } catch (err) { + console.error("Could not get path for file:", file.name, err); + return file.name; + } + }); + + console.log("Paths to insert:", paths); + + // Insert the path(s) into the terminal + // If multiple files, separate with spaces and quote if necessary + const pathString = paths.map(path => { + // Quote paths that contain spaces + if (path.includes(" ")) { + return `"${path}"`; + } + return path; + }).join(" "); + + console.log("Final path string:", pathString); + + // Send the path to the terminal + if (model.termRef.current && pathString) { + model.sendDataToController(pathString); + } + }, [model]); + + const handleDragEnter = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDragLeave = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + return ( -
+
{termBg &&
} diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 8ecaef08c..b6abb0674 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -28,6 +28,61 @@ const TermCacheFileName = "cache:term:full"; const MinDataProcessedForCache = 100 * 1024; export const SupportsImageInput = true; +// Convert terminal selection to markdown preserving formatting +function convertSelectionToMarkdown(terminal: Terminal): string { + const selection = terminal.getSelectionPosition(); + if (!selection) { + return terminal.getSelection(); + } + + let markdown = ""; + const buffer = terminal.buffer.active; + + for (let y = selection.start.y; y <= selection.end.y; y++) { + const line = buffer.getLine(y); + if (!line) continue; + + const startCol = y === selection.start.y ? selection.start.x : 0; + const endCol = y === selection.end.y ? selection.end.x : line.length; + + let lineText = ""; + let isBold = false; + let currentSegment = ""; + + for (let x = startCol; x < endCol; x++) { + const cell = line.getCell(x); + if (!cell) continue; + + const char = cell.getChars(); + // Check if cell has bold attribute (bit 0 of fg color flags) + const cellBold = (cell.getBgColorMode() & 0x01) !== 0 || (cell.getFgColorMode() & 0x01) !== 0; + + // Handle bold transitions + if (cellBold !== isBold) { + if (currentSegment) { + lineText += isBold ? `**${currentSegment}**` : currentSegment; + currentSegment = ""; + } + isBold = cellBold; + } + + currentSegment += char || " "; + } + + // Flush remaining segment + if (currentSegment) { + lineText += isBold ? `**${currentSegment}**` : currentSegment; + } + + markdown += lineText.trimEnd(); + if (y < selection.end.y) { + markdown += "\n"; + } + } + + return markdown; +} + // detect webgl support function detectWebGLSupport(): boolean { try { @@ -481,13 +536,29 @@ export class TermWrap { if (!globalStore.get(copyOnSelectAtom)) { return; } - const selectedText = this.terminal.getSelection(); - if (selectedText.length > 0) { - navigator.clipboard.writeText(selectedText); + const markdownText = convertSelectionToMarkdown(this.terminal); + if (markdownText.length > 0) { + navigator.clipboard.writeText(markdownText); } }) ) ); + + // Intercept copy events to provide markdown formatting + const copyHandler = (e: ClipboardEvent) => { + const selection = this.terminal.getSelection(); + if (selection.length > 0) { + e.preventDefault(); + const markdownText = convertSelectionToMarkdown(this.terminal); + e.clipboardData?.setData("text/plain", markdownText); + } + }; + this.connectElem.addEventListener("copy", copyHandler); + this.toDispose.push({ + dispose: () => { + this.connectElem.removeEventListener("copy", copyHandler); + }, + }); if (this.onSearchResultsDidChange != null) { this.toDispose.push(this.searchAddon.onDidChangeResults(this.onSearchResultsDidChange.bind(this))); } @@ -701,7 +772,21 @@ export class TermWrap { handleResize() { const oldRows = this.terminal.rows; const oldCols = this.terminal.cols; + + // Preserve scroll position before resize + const wasAtBottom = this.terminal.buffer.active.baseY + this.terminal.rows >= this.terminal.buffer.active.length; + const scrollY = this.terminal.buffer.active.viewportY; + this.fitAddon.fit(); + + // Restore scroll position after resize + if (!wasAtBottom && scrollY > 0) { + // If user wasn't at bottom, try to keep them at the same content + setTimeout(() => { + this.terminal.scrollToLine(scrollY); + }, 0); + } + if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) { const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols }; const wsCommand: SetBlockTermSizeWSCommand = { diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 3476fe539..480c2063f 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -130,6 +130,7 @@ declare global { openBuilder: (appId?: string) => void; // open-builder setBuilderWindowAppId: (appId: string) => void; // set-builder-window-appid doRefresh: () => void; // do-refresh + getPathForFile: (file: File) => string; // uses webUtils.getPathForFile }; type ElectronContextMenuItem = { diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 23a900839..56b9253b3 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1222,6 +1222,7 @@ declare global { "settings:customsettings"?: number; "settings:customaimodes"?: number; "settings:secretscount"?: number; + "settings:transparent"?: boolean; "activity:activeminutes"?: number; "activity:fgminutes"?: number; "activity:openminutes"?: number; @@ -1306,6 +1307,7 @@ declare global { "settings:customsettings"?: number; "settings:customaimodes"?: number; "settings:secretscount"?: number; + "settings:transparent"?: boolean; }; // waveobj.Tab diff --git a/package-lock.json b/package-lock.json index 811475533..a2222c504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -192,6 +192,7 @@ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -212,6 +213,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -471,6 +473,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.37.0.tgz", "integrity": "sha512-DAFVUvEg+u7jUs6BZiVz9zdaUebYULPiQ4LM2R4n8Nujzyj7BZzGr2DCd85ip4p/cx7nAZWKM8pLcGtkTRTdsg==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.37.0", "@algolia/requester-browser-xhr": "5.37.0", @@ -624,6 +627,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2482,6 +2486,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2504,6 +2509,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2613,6 +2619,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3005,6 +3012,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4078,6 +4086,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.1.tgz", "integrity": "sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.8.1", "@docusaurus/logger": "3.8.1", @@ -5425,7 +5434,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -5447,7 +5455,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -6143,7 +6150,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6167,7 +6173,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6191,7 +6196,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6209,7 +6213,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6227,7 +6230,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6245,7 +6247,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6263,7 +6264,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6281,7 +6281,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6299,7 +6298,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6317,7 +6315,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6335,7 +6332,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6353,7 +6349,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6377,7 +6372,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6401,7 +6395,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6425,7 +6418,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6449,7 +6441,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6473,7 +6464,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6497,7 +6487,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6518,7 +6507,6 @@ "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.4.4" }, @@ -6542,7 +6530,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6563,7 +6550,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6584,7 +6570,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6857,6 +6842,7 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -8657,6 +8643,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -9051,7 +9038,8 @@ "version": "0.0.7", "resolved": "https://registry.npmjs.org/@table-nav/core/-/core-0.0.7.tgz", "integrity": "sha512-pCh18jHDRe3tw9sJZXfKi4cSD6VjHbn40CYdqhp5X91SIX7rakDEQAsTx6F7Fv9TUv265l+5rUDcYNaJ0N0cqQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@table-nav/react": { "version": "0.0.7", @@ -10098,6 +10086,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.2.tgz", "integrity": "sha512-lif9hF9afNk39jMUVYk5eyYEojLZQqaYX61LfuwUJJ1+qiQbh7jVaZXskYgzyjAIFDFQRf5Sd6MVM7EyXkfiRw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -10173,6 +10162,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -10459,6 +10449,7 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -11133,7 +11124,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", @@ -11188,6 +11180,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -11296,6 +11289,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -11360,6 +11354,7 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.37.0.tgz", "integrity": "sha512-y7gau/ZOQDqoInTQp0IwTOjkrHc4Aq4R8JgpmCleFwiLl+PbN2DMWoDUWZnrK8AhNJwT++dn28Bt4NZYNLAmuA==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.3.0", "@algolia/client-abtesting": "5.37.0", @@ -12191,6 +12186,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.2", "caniuse-lite": "^1.0.30001741", @@ -12759,6 +12755,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -13653,8 +13650,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -13822,6 +13818,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -14152,6 +14149,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -14561,6 +14559,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -14990,6 +14989,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -15465,7 +15465,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -15486,7 +15485,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -15502,7 +15500,6 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -15513,7 +15510,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -15827,6 +15823,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -22921,7 +22918,8 @@ "version": "0.52.2", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/monaco-languageserver-types": { "version": "0.4.0", @@ -23615,7 +23613,8 @@ "version": "2.12.0", "resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.12.0.tgz", "integrity": "sha512-mWJ5MOkcZ/ljHwfLw8+bN0V9ziGCoNoqULcp994j5DTGNQvnkWKWkA7rnO29Kyew5AoHxUnJ4Ndqfcl0HSQjXg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/overlayscrollbars-react": { "version": "0.5.6", @@ -24390,6 +24389,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -25293,6 +25293,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -25889,7 +25890,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -25907,7 +25907,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -26159,6 +26158,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -26389,6 +26389,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -26437,6 +26438,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -26526,6 +26528,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -26554,6 +26557,7 @@ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", "license": "MIT", + "peer": true, "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -26591,6 +26595,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -28355,6 +28360,7 @@ "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -28521,6 +28527,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -28639,6 +28646,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -29039,61 +29047,6 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, - "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" - } - }, - "node_modules/sharp/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -30034,7 +29987,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", @@ -30062,7 +30014,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=16" } @@ -30117,7 +30068,8 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -30173,7 +30125,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -30215,7 +30166,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -30237,7 +30187,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -30251,7 +30200,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -30266,7 +30214,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -31411,6 +31358,7 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", + "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -32319,6 +32267,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -32575,6 +32524,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -32817,6 +32767,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -33753,6 +33704,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -33802,7 +33754,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "tsunami/frontend/node_modules/redux-thunk": { "version": "3.1.0", diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index a255c680a..ae73fed92 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -319,6 +319,12 @@ func HandleAppendBlockFile(blockId string, blockFile string, data []byte) error Data64: base64.StdEncoding.EncodeToString(data), }, }) + + // Check if we should auto-generate a tab title + if blockFile == wavebase.BlockFile_Term { + CheckAndGenerateTitle(blockId, data) + } + return nil } diff --git a/pkg/blockcontroller/tabtitle_trigger.go b/pkg/blockcontroller/tabtitle_trigger.go new file mode 100644 index 000000000..b6e2b6321 --- /dev/null +++ b/pkg/blockcontroller/tabtitle_trigger.go @@ -0,0 +1,105 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package blockcontroller + +import ( + "bytes" + "context" + "log" + "sync" + "time" + + "github.com/wavetermdev/waveterm/pkg/waveai" + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wstore" +) + +const ( + LinesThresholdForTitle = 1 // Generate title after N lines of output + TitleCooldownSeconds = 10 // Don't regenerate title more often than every 10 seconds +) + +// tabTitleTracker tracks line counts per tab for auto-generating titles +type tabTitleTracker struct { + mu sync.Mutex + tabLineCounts map[string]int // tabId -> line count + lastTitleGenTime map[string]time.Time // tabId -> last time title was generated +} + +var titleTracker = &tabTitleTracker{ + tabLineCounts: make(map[string]int), + lastTitleGenTime: make(map[string]time.Time), +} + +// CheckAndGenerateTitle checks if we should generate a title for the tab containing this block +func CheckAndGenerateTitle(blockId string, data []byte) { + // Count newlines in the data + newlines := bytes.Count(data, []byte("\n")) + if newlines == 0 { + return // No new lines, nothing to do + } + + // Get the tab that contains this block + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + block, err := wstore.DBGet[*waveobj.Block](ctx, blockId) + if err != nil || block == nil { + return + } + + // Extract tabId from parent ORef (format: "tab:uuid") + if block.ParentORef == "" { + return + } + oref, err := waveobj.ParseORef(block.ParentORef) + if err != nil || oref.OType != waveobj.OType_Tab { + return + } + tabId := oref.OID + + // Update line count and check threshold + titleTracker.mu.Lock() + titleTracker.tabLineCounts[tabId] += newlines + lineCount := titleTracker.tabLineCounts[tabId] + lastGenTime, exists := titleTracker.lastTitleGenTime[tabId] + titleTracker.mu.Unlock() + + // Check if we've hit the threshold + if lineCount < LinesThresholdForTitle { + return + } + + // Check cooldown period + if exists && time.Since(lastGenTime).Seconds() < TitleCooldownSeconds { + return + } + + // Reset counter and update last gen time before generating (to prevent duplicates) + titleTracker.mu.Lock() + titleTracker.tabLineCounts[tabId] = 0 + titleTracker.lastTitleGenTime[tabId] = time.Now() + titleTracker.mu.Unlock() + + // Generate title asynchronously + go func() { + genCtx, genCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer genCancel() + + title, err := waveai.GenerateTabTitle(genCtx, tabId) + if err != nil { + log.Printf("Error generating tab title for tab %s: %v", tabId, err) + return + } + + // Update the tab name + err = wstore.UpdateTabName(genCtx, tabId, title) + if err != nil { + log.Printf("Error updating tab name for tab %s: %v", tabId, err) + return + } + + log.Printf("Auto-generated tab title for tab %s: %q", tabId, title) + }() +} diff --git a/pkg/waveai/tabtitle.go b/pkg/waveai/tabtitle.go new file mode 100644 index 000000000..9703ec164 --- /dev/null +++ b/pkg/waveai/tabtitle.go @@ -0,0 +1,70 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package waveai + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wstore" +) + +// GenerateTabTitle generates a short title for a tab based on the current working directory +func GenerateTabTitle(ctx context.Context, tabId string) (string, error) { + // Get the tab + tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) + if err != nil { + return "", fmt.Errorf("error getting tab: %w", err) + } + + // If no blocks, return default + if len(tab.BlockIds) == 0 { + return "", fmt.Errorf("tab has no blocks") + } + + // Get the first block (usually the primary terminal) + blockId := tab.BlockIds[0] + block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId) + if err != nil { + return "", fmt.Errorf("error getting block: %w", err) + } + + // Get the current working directory from block metadata + meta := waveobj.GetMeta(block) + cwd, ok := meta[waveobj.MetaKey_CmdCwd].(string) + if !ok || cwd == "" { + return "", fmt.Errorf("no working directory available") + } + + // Generate title from the last 2 folders + title := generateTitleFromPath(cwd) + return title, nil +} + +// generateTitleFromPath creates a title from the last folder in a path +func generateTitleFromPath(fullPath string) string { + // Clean the path (remove trailing slashes, etc.) + cleanPath := filepath.Clean(fullPath) + + // Split the path into components + parts := strings.Split(cleanPath, string(filepath.Separator)) + + // Filter out empty parts + var nonEmptyParts []string + for _, part := range parts { + if part != "" { + nonEmptyParts = append(nonEmptyParts, part) + } + } + + if len(nonEmptyParts) == 0 { + return "/" + } + + // Use just the last folder name + return nonEmptyParts[len(nonEmptyParts)-1] +} diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index 9037b2e5f..da4b68119 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -199,16 +199,36 @@ func getTabPresetMeta() (waveobj.MetaMapType, error) { // returns tabid func CreateTab(ctx context.Context, workspaceId string, tabName string, activateTab bool, pinned bool, isInitialLaunch bool) (string, error) { + ws, err := GetWorkspace(ctx, workspaceId) + if err != nil { + return "", fmt.Errorf("workspace %s not found: %w", workspaceId, err) + } + if tabName == "" { - ws, err := GetWorkspace(ctx, workspaceId) - if err != nil { - return "", fmt.Errorf("workspace %s not found: %w", workspaceId, err) - } tabName = "T" + fmt.Sprint(len(ws.TabIds)+len(ws.PinnedTabIds)+1) } + // Try to inherit cwd from the active tab + var inheritedMeta waveobj.MetaMapType + if ws.ActiveTabId != "" && !isInitialLaunch { + activeTab, _ := wstore.DBGet[*waveobj.Tab](ctx, ws.ActiveTabId) + if activeTab != nil && len(activeTab.BlockIds) > 0 { + // Get the first block from the active tab + firstBlock, _ := wstore.DBGet[*waveobj.Block](ctx, activeTab.BlockIds[0]) + if firstBlock != nil { + meta := waveobj.GetMeta(firstBlock) + if cwd, ok := meta[waveobj.MetaKey_CmdCwd].(string); ok && cwd != "" { + // Inherit the cwd for the new tab + inheritedMeta = waveobj.MetaMapType{ + waveobj.MetaKey_CmdCwd: cwd, + } + } + } + } + } + // The initial tab for the initial launch should be pinned - tab, err := createTabObj(ctx, workspaceId, tabName, pinned || isInitialLaunch, nil) + tab, err := createTabObjWithWorkspace(ctx, ws, tabName, pinned || isInitialLaunch, inheritedMeta) if err != nil { return "", fmt.Errorf("error creating tab: %w", err) } @@ -221,7 +241,17 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate // No need to apply an initial layout for the initial launch, since the starter layout will get applied after onboarding modal dismissal if !isInitialLaunch { - err = ApplyPortableLayout(ctx, tab.OID, GetNewTabLayout(), true) + newTabLayout := GetNewTabLayout() + // Merge inherited cwd into the terminal block's meta + if len(inheritedMeta) > 0 && len(newTabLayout) > 0 { + if newTabLayout[0].BlockDef.Meta == nil { + newTabLayout[0].BlockDef.Meta = make(waveobj.MetaMapType) + } + for k, v := range inheritedMeta { + newTabLayout[0].BlockDef.Meta[k] = v + } + } + err = ApplyPortableLayout(ctx, tab.OID, newTabLayout, true) if err != nil { return tab.OID, fmt.Errorf("error applying new tab layout: %w", err) } @@ -240,11 +270,8 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate return tab.OID, nil } -func createTabObj(ctx context.Context, workspaceId string, name string, pinned bool, meta waveobj.MetaMapType) (*waveobj.Tab, error) { - ws, err := GetWorkspace(ctx, workspaceId) - if err != nil { - return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err) - } +// createTabObjWithWorkspace creates a tab object with an already-fetched workspace +func createTabObjWithWorkspace(ctx context.Context, ws *waveobj.Workspace, name string, pinned bool, meta waveobj.MetaMapType) (*waveobj.Tab, error) { layoutStateId := uuid.NewString() tab := &waveobj.Tab{ OID: uuid.NewString(), @@ -267,6 +294,15 @@ func createTabObj(ctx context.Context, workspaceId string, name string, pinned b return tab, nil } +// createTabObj is a wrapper that fetches the workspace and calls createTabObjWithWorkspace +func createTabObj(ctx context.Context, workspaceId string, name string, pinned bool, meta waveobj.MetaMapType) (*waveobj.Tab, error) { + ws, err := GetWorkspace(ctx, workspaceId) + if err != nil { + return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err) + } + return createTabObjWithWorkspace(ctx, ws, name, pinned, meta) +} + // Must delete all blocks individually first. // Also deletes LayoutState. // recursive: if true, will recursively close parent window, workspace, if they are empty. diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 0490b403f..489cb8453 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -356,6 +356,12 @@ func FocusWindowCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) er return err } +// command "generatetabtitle", wshserver.GenerateTabTitleCommand +func GenerateTabTitleCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (string, error) { + resp, err := sendRpcRequestCallHelper[string](w, "generatetabtitle", data, opts) + return resp, err +} + // command "getbuilderoutput", wshserver.GetBuilderOutputCommand func GetBuilderOutputCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) ([]string, error) { resp, err := sendRpcRequestCallHelper[[]string](w, "getbuilderoutput", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 0191a4e4e..54ec5f3df 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -186,6 +186,9 @@ const ( Command_GetSecretsNames = "getsecretsnames" Command_SetSecrets = "setsecrets" Command_GetSecretsLinuxStorageBackend = "getsecretslinuxstoragebackend" + + // tab + Command_GenerateTabTitle = "generatetabtitle" ) type RespOrErrorUnion[T any] struct { @@ -261,6 +264,7 @@ type WshRpcInterface interface { FetchSuggestionsCommand(ctx context.Context, data FetchSuggestionsData) (*FetchSuggestionsResponse, error) DisposeSuggestionsCommand(ctx context.Context, widgetId string) error GetTabCommand(ctx context.Context, tabId string) (*waveobj.Tab, error) + GenerateTabTitleCommand(ctx context.Context, tabId string) (string, error) // connection functions ConnStatusCommand(ctx context.Context) ([]ConnStatus, error) diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index f211d71e7..fe7237d20 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -1408,6 +1408,14 @@ func (ws *WshServer) GetTabCommand(ctx context.Context, tabId string) (*waveobj. return tab, nil } +func (ws *WshServer) GenerateTabTitleCommand(ctx context.Context, tabId string) (string, error) { + title, err := waveai.GenerateTabTitle(ctx, tabId) + if err != nil { + return "", fmt.Errorf("error generating tab title: %w", err) + } + return title, nil +} + func (ws *WshServer) GetSecretsCommand(ctx context.Context, names []string) (map[string]string, error) { result := make(map[string]string) for _, name := range names {