From 0a2e3a94e50f143ba7500b900ada5974be18ae40 Mon Sep 17 00:00:00 2001 From: Cowhen Date: Sat, 18 Apr 2026 09:55:45 +0200 Subject: [PATCH 1/4] Add block rename and preview follow terminal features - Add inline block renaming via context menu with frame:title metadata - Add preview follow terminal functionality to sync directory navigation - Configure dev build as separate Wave Dev app to avoid conflicts - Add launch_wave_dev.command helper script for dev builds Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 13 + frontend/app/block/blockframe-header.tsx | 58 ++ frontend/app/block/blockrenamestate.ts | 15 + frontend/app/view/preview/preview-model.tsx | 52 ++ frontend/app/view/preview/preview.tsx | 149 ++++- frontend/types/gotypes.d.ts | 2 + launch_wave_dev.command | 4 + package-lock.json | 600 ++------------------ package.json | 4 +- pkg/waveobj/metaconsts.go | 3 + pkg/waveobj/wtypemeta.go | 3 + 11 files changed, 350 insertions(+), 553 deletions(-) create mode 100644 frontend/app/block/blockrenamestate.ts create mode 100755 launch_wave_dev.command diff --git a/CLAUDE.md b/CLAUDE.md index ea0daa9425..124b47ef5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,3 +16,16 @@ This project uses a set of "skill" guides — focused how-to documents for commo | electron-api | `.kilocode/skills/electron-api/SKILL.md` | Guide for adding new Electron APIs to Wave Terminal. Use when implementing new frontend-to-electron communications via preload/IPC. | | waveenv | `.kilocode/skills/waveenv/SKILL.md` | Guide for creating WaveEnv narrowings in Wave Terminal. Use when writing a named subset type of WaveEnv for a component tree, documenting environmental dependencies, or enabling mock environments for preview/test server usage. | | wps-events | `.kilocode/skills/wps-events/SKILL.md` | Guide for working with Wave Terminal's WPS (Wave PubSub) event system. Use when implementing new event types, publishing events, subscribing to events, or adding asynchronous communication between components. | + +--- + +## Dev Build & Packaging + +- **App ID & Name**: Changed to `dev.commandline.waveterm.custom` and productName to `Wave Dev` in `package.json` so the dev build appears as a completely separate app to macOS (avoids Electron single-instance lock conflict and Launch Services confusion). +- **Build**: `task package` (requires `PATH="/opt/homebrew/bin:$PATH"` for Go/Task). Builds as `Wave Dev.app` in `make/mac-arm64/`. +- **Launch**: Use `launch_wave_dev.command` or run directly: + ```bash + WAVETERM_HOME=~/.waveterm-dev make/mac-arm64/Wave\ Dev.app/Contents/MacOS/Wave\ Dev + ``` + `WAVETERM_HOME` gives the dev build a separate data directory (`~/.waveterm-dev`) from the vanilla install (`~/.waveterm`). +- **Do not modify Info.plist or re-sign** the built app bundle — it breaks code signing on macOS and causes crashes. diff --git a/frontend/app/block/blockframe-header.tsx b/frontend/app/block/blockframe-header.tsx index a70f323e71..4223397a77 100644 --- a/frontend/app/block/blockframe-header.tsx +++ b/frontend/app/block/blockframe-header.tsx @@ -21,6 +21,7 @@ import { import { globalStore } from "@/app/store/jotaiStore"; import { uxCloseBlock } from "@/app/store/keymodel"; import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { renamingBlockIdAtom, startBlockRename, stopBlockRename } from "@/app/block/blockrenamestate"; import { useWaveEnv } from "@/app/waveenv/waveenv"; import { IconButton } from "@/element/iconbutton"; import { NodeModel } from "@/layout/index"; @@ -42,6 +43,10 @@ function handleHeaderContextMenu( e.stopPropagation(); const magnified = globalStore.get(nodeModel.isMagnified); const menu: ContextMenuItem[] = [ + { + label: "Rename Block", + click: () => startBlockRename(blockId), + }, { label: magnified ? "Un-Magnify Block" : "Magnify Block", click: () => { @@ -78,11 +83,64 @@ type HeaderTextElemsProps = { const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: HeaderTextElemsProps) => { const waveEnv = useWaveEnv(); const frameTextAtom = waveEnv.getBlockMetaKeyAtom(blockId, "frame:text"); + const frameTitleAtom = waveEnv.getBlockMetaKeyAtom(blockId, "frame:title"); const frameText = jotai.useAtomValue(frameTextAtom); + const frameTitle = jotai.useAtomValue(frameTitleAtom); + const renamingBlockId = jotai.useAtomValue(renamingBlockIdAtom); + const isRenaming = renamingBlockId === blockId; + const useTermHeader = util.useAtomValueSafe(viewModel?.useTermHeader); let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText); headerTextUnion = frameText ?? headerTextUnion; + const saveRename = React.useCallback( + (newTitle: string) => { + const val = newTitle.trim() || null; + waveEnv.rpc.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", blockId), + meta: { "frame:title": val }, + }); + stopBlockRename(); + }, + [blockId, waveEnv] + ); + + if (isRenaming) { + return ( +
+ saveRename(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + saveRename(e.currentTarget.value); + } else if (e.key === "Escape") { + stopBlockRename(); + } + }} + onClick={(e) => e.stopPropagation()} + /> +
+ ); + } + const headerTextElems: React.ReactElement[] = []; + + // For terminal blocks, show frame:title as a name badge in the text area + if (useTermHeader && frameTitle) { + headerTextElems.push( +
+ {frameTitle} +
+ ); + } + if (typeof headerTextUnion === "string") { if (!util.isBlank(headerTextUnion)) { headerTextElems.push( diff --git a/frontend/app/block/blockrenamestate.ts b/frontend/app/block/blockrenamestate.ts new file mode 100644 index 0000000000..93fe2c49c9 --- /dev/null +++ b/frontend/app/block/blockrenamestate.ts @@ -0,0 +1,15 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { globalStore } from "@/app/store/jotaiStore"; +import * as jotai from "jotai"; + +export const renamingBlockIdAtom = jotai.atom(null); + +export function startBlockRename(blockId: string) { + globalStore.set(renamingBlockIdAtom, blockId); +} + +export function stopBlockRename() { + globalStore.set(renamingBlockIdAtom, null); +} diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index 8315e48b2a..2748c19910 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -170,6 +170,11 @@ export class PreviewModel implements ViewModel { codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; env: PreviewEnv; + followTermIdAtom: Atom; + followTermCwdAtom: Atom; + followTermBidirAtom: Atom; + followTermMenuDataAtom: PrimitiveAtom<{ pos: { x: number; y: number }; terms: { blockId: string; title: string }[]; currentFollowId: string | null; bidir: boolean } | null>; + constructor({ blockId, nodeModel, tabModel, waveEnv }: ViewModelInitType) { this.viewType = "preview"; this.blockId = blockId; @@ -334,6 +339,7 @@ export class PreviewModel implements ViewModel { const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit"; if (mimeType == "directory") { const showHiddenFiles = get(this.showHiddenFiles); + const followTermId = get(this.followTermIdAtom); return [ { elemtype: "iconbutton", @@ -343,6 +349,13 @@ export class PreviewModel implements ViewModel { globalStore.set(this.showHiddenFiles, (prev) => !prev); }, }, + { + elemtype: "iconbutton", + icon: "link", + title: followTermId ? "Following Terminal (click to change or unlink)" : "Follow a Terminal", + iconColor: followTermId ? "var(--success-color)" : undefined, + click: (e: React.MouseEvent) => this.showFollowTermMenu(e), + }, { elemtype: "iconbutton", icon: "arrows-rotate", @@ -489,6 +502,45 @@ export class PreviewModel implements ViewModel { }); this.noPadding = atom(true); + this.followTermIdAtom = atom((get) => { + return (get(this.blockAtom)?.meta?.["preview:followtermid"] as string) ?? null; + }); + this.followTermCwdAtom = atom((get) => { + const termId = get(this.followTermIdAtom); + if (!termId) return null; + const termBlock = WOS.getObjectValue(WOS.makeORef("block", termId), get); + return (termBlock?.meta?.["cmd:cwd"] as string) ?? null; + }); + this.followTermBidirAtom = atom((get) => { + return (get(this.blockAtom)?.meta?.["preview:followterm:bidir"] as boolean) ?? false; + }); + this.followTermMenuDataAtom = atom(null); + } + + showFollowTermMenu(e: React.MouseEvent) { + const tabData = globalStore.get(this.tabModel.tabAtom); + const blockIds = tabData?.blockids ?? []; + const terms = blockIds + .filter((bid) => bid !== this.blockId) + .map((bid) => { + const block = WOS.getObjectValue(WOS.makeORef("block", bid), globalStore.get); + return { blockId: bid, block }; + }) + .filter(({ block }) => block?.meta?.view === "term") + .map(({ blockId: bid, block }, i) => ({ + blockId: bid, + title: (block?.meta?.["frame:title"] as string) || `Terminal ${i + 1}`, + })); + + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const currentFollowId = globalStore.get(this.followTermIdAtom); + const bidir = globalStore.get(this.followTermBidirAtom); + globalStore.set(this.followTermMenuDataAtom, { + pos: { x: rect.left, y: rect.bottom + 4 }, + terms, + currentFollowId, + bidir, + }); } markdownShowTocToggle() { diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 87cf44678a..b18806f30a 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -4,11 +4,28 @@ import { CenteredDiv } from "@/app/element/quickelems"; import { globalStore } from "@/app/store/jotaiStore"; import { TabRpcClient } from "@/app/store/wshrpcutil"; + +function shellEscapePath(path: string): string { + if (path === "~") return "~"; + if (path.startsWith("~/")) { + // ~ must be unquoted to expand; single-quote the rest + return "~/" + "'" + path.slice(2).replace(/'/g, "'\\''") + "'"; + } + return "'" + path.replace(/'/g, "'\\''") + "'"; +} + +async function sendCdToTerminal(termBlockId: string, path: string, env: import("./previewenv").PreviewEnv) { + const command = "\x15cd " + shellEscapePath(path) + "\r"; + await env.rpc.ControllerInputCommand(TabRpcClient, { blockid: termBlockId, inputdata64: stringToBase64(command) }); +} import { BlockHeaderSuggestionControl } from "@/app/suggestion/suggestion"; import { useWaveEnv } from "@/app/waveenv/waveenv"; -import { isBlank, makeConnRoute } from "@/util/util"; +import { BlockModel } from "@/app/block/block-model"; +import * as WOS from "@/store/wos"; +import { fireAndForget, isBlank, makeConnRoute, stringToBase64 } from "@/util/util"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { memo, useEffect } from "react"; +import { memo, useEffect, useRef } from "react"; +import ReactDOM from "react-dom"; import { CSVView } from "./csvview"; import { DirectoryPreview } from "./preview-directory"; import { CodeEditPreview } from "./preview-edit"; @@ -96,6 +113,110 @@ const fetchSuggestions = async ( }); }; +function FollowTermDropdown({ model }: { model: PreviewModel }) { + const menuData = useAtomValue(model.followTermMenuDataAtom); + + if (!menuData) return null; + + const { pos, terms, currentFollowId, bidir } = menuData; + const closeMenu = () => { + BlockModel.getInstance().setBlockHighlight(null); + globalStore.set(model.followTermMenuDataAtom, null); + }; + const linkTerm = (blockId: string) => { + BlockModel.getInstance().setBlockHighlight(null); + fireAndForget(async () => { + await model.env.services.object.UpdateObjectMeta(WOS.makeORef("block", model.blockId), { + "preview:followtermid": blockId, + }); + }); + globalStore.set(model.followTermMenuDataAtom, null); + }; + const toggleBidir = () => { + fireAndForget(async () => { + await model.env.services.object.UpdateObjectMeta(WOS.makeORef("block", model.blockId), { + "preview:followterm:bidir": !bidir, + }); + }); + globalStore.set(model.followTermMenuDataAtom, { ...menuData, bidir: !bidir }); + }; + const unlink = () => { + fireAndForget(async () => { + await model.env.services.object.UpdateObjectMeta(WOS.makeORef("block", model.blockId), { + "preview:followtermid": null, + "preview:followterm:bidir": null, + }); + }); + globalStore.set(model.followTermMenuDataAtom, null); + }; + + const dropdownStyle: React.CSSProperties = { + left: pos.x, + top: pos.y, + background: "var(--modal-bg-color)", + border: "1px solid var(--border-color)", + boxShadow: "0px 8px 24px 0px rgba(0,0,0,0.4)", + borderRadius: "var(--modal-border-radius)", + }; + const dividerStyle: React.CSSProperties = { borderTop: "1px solid var(--border-color)" }; + + return ReactDOM.createPortal( + <> +
+
+ {terms.length === 0 ? ( +
No terminals on this tab
+ ) : ( + terms.map(({ blockId, title }) => ( +
+ BlockModel.getInstance().setBlockHighlight({ blockId, icon: "terminal" }) + } + onMouseLeave={() => BlockModel.getInstance().setBlockHighlight(null)} + onMouseDown={(e) => e.stopPropagation()} + onClick={() => linkTerm(blockId)} + > + + {title} +
+ )) + )} + {currentFollowId && ( + <> +
+
e.stopPropagation()} + onClick={toggleBidir} + > + + Bidirectional +
+
+
e.stopPropagation()} + onClick={unlink} + > + Stop Following +
+ + )} +
+ , + document.body + ); +} + function PreviewView({ blockRef, contentRef, @@ -111,6 +232,11 @@ function PreviewView({ const [errorMsg, setErrorMsg] = useAtom(model.errorMsgAtom); const connection = useAtomValue(model.connectionImmediate); const fileInfo = useAtomValue(model.statFile); + const followTermId = useAtomValue(model.followTermIdAtom); + const followTermCwd = useAtomValue(model.followTermCwdAtom); + const followTermBidir = useAtomValue(model.followTermBidirAtom); + const loadableFileInfo = useAtomValue(model.loadableFileInfo); + const suppressBidirRef = useRef(false); useEffect(() => { console.log("fileInfo or connection changed", fileInfo, connection); @@ -120,6 +246,24 @@ function PreviewView({ setErrorMsg(null); }, [connection, fileInfo]); + useEffect(() => { + if (!followTermId || !followTermCwd) return; + suppressBidirRef.current = true; + fireAndForget(() => model.goHistory(followTermCwd)); + }, [followTermCwd]); + + useEffect(() => { + if (!followTermId || !followTermBidir) return; + if (suppressBidirRef.current) { + suppressBidirRef.current = false; + return; + } + if (loadableFileInfo.state !== "hasData") return; + const fi = loadableFileInfo.data; + if (!fi || fi.mimetype !== "directory" || !fi.path) return; + fireAndForget(() => sendCdToTerminal(followTermId, fi.path, env)); + }, [loadableFileInfo]); + if (connStatus?.status != "connected") { return null; } @@ -148,6 +292,7 @@ function PreviewView({ return ( <> +
{errorMsg && setErrorMsg(null)} />}
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index c5b870d7ed..87a8d4efbc 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1196,6 +1196,8 @@ declare global { "web:useragenttype"?: string; "markdown:fontsize"?: number; "markdown:fixedfontsize"?: number; + "preview:followtermid"?: string; + "preview:followterm:bidir"?: boolean; "tsunami:*"?: boolean; "tsunami:sdkreplacepath"?: string; "tsunami:apppath"?: string; diff --git a/launch_wave_dev.command b/launch_wave_dev.command new file mode 100755 index 0000000000..cf3e85d1e5 --- /dev/null +++ b/launch_wave_dev.command @@ -0,0 +1,4 @@ +#!/bin/bash +# Launch Wave (dev build) with a separate data directory +# Double-click this file in Finder to launch, or run from terminal. +WAVETERM_HOME="$HOME/.waveterm-dev" exec /Users/zmd2hi/Documents/00_personal/wave_term/make/mac-arm64/Wave\ Dev.app/Contents/MacOS/Wave\ Dev diff --git a/package-lock.json b/package-lock.json index b219e6cc92..f3a4a0502e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,6 +178,7 @@ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -198,6 +199,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" }, @@ -457,6 +459,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", @@ -610,6 +613,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", @@ -2468,6 +2472,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2490,6 +2495,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2599,6 +2605,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3020,6 +3027,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4071,6 +4079,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4833,7 +4842,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -4848,18 +4856,6 @@ "node": ">=14.14" } }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -5576,468 +5572,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6407,6 +5941,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" }, @@ -8248,6 +7783,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", @@ -8642,7 +8178,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", @@ -9664,6 +9201,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" } @@ -9739,6 +9277,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" } @@ -10020,6 +9559,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -10714,6 +10254,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" }, @@ -10809,6 +10350,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -10873,6 +10415,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", @@ -11829,6 +11372,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -12337,6 +11881,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.1", "@chevrotain/gast": "11.1.1", @@ -13196,8 +12741,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", @@ -13365,6 +12909,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13695,6 +13240,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" } @@ -14104,6 +13650,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" } @@ -14549,6 +14096,7 @@ "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", @@ -15054,7 +14602,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -15075,7 +14622,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -15091,7 +14637,6 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -15102,7 +14647,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -15432,6 +14976,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -20284,6 +19829,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -22598,7 +22144,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -23407,7 +22952,8 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.14.0.tgz", "integrity": "sha512-RjV0pqc79kYhQLC3vTcLRb5GLpI1n6qh0Oua3g+bGH4EgNOJHVBGP7u0zZtxoAa0dkHlAqTTSYRb9MMmxNLjig==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/overlayscrollbars-react": { "version": "0.5.6", @@ -24187,6 +23733,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -25090,6 +24637,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -25689,7 +25237,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -25707,7 +25254,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -25808,6 +25354,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -25959,6 +25506,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", @@ -26186,6 +25734,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -26234,6 +25783,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" }, @@ -26307,6 +25857,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": "*" }, @@ -26372,6 +25923,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", @@ -28120,7 +27672,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -28135,7 +27686,6 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -28157,7 +27707,6 @@ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -28196,6 +27745,7 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -28365,6 +27915,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", @@ -28483,6 +28034,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -28874,61 +28426,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", @@ -29812,7 +29309,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", @@ -29840,7 +29336,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=16" } @@ -29968,7 +29463,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -30487,7 +29981,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsunami-frontend": { "resolved": "tsunami/frontend", @@ -31068,6 +30563,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -31166,6 +30662,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", @@ -32048,6 +31545,7 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -32208,6 +31706,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -32450,6 +31949,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", @@ -33478,6 +32978,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" @@ -33527,7 +33028,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/package.json b/package.json index 781a6a45fe..134e8f9d70 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,13 @@ "name": "Command Line Inc", "email": "info@commandline.dev" }, - "productName": "Wave", + "productName": "Wave Dev", "description": "Open-Source AI-Native Terminal Built for Seamless Workflows", "license": "Apache-2.0", "version": "0.14.5", "homepage": "https://waveterm.dev", "build": { - "appId": "dev.commandline.waveterm" + "appId": "dev.commandline.waveterm.custom" }, "private": true, "main": "./dist/main/index.js", diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index 0ce08099d8..599b5685e2 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -138,6 +138,9 @@ const ( MetaKey_MarkdownFontSize = "markdown:fontsize" MetaKey_MarkdownFixedFontSize = "markdown:fixedfontsize" + MetaKey_PreviewFollowTermId = "preview:followtermid" + MetaKey_PreviewFollowTermBidir = "preview:followterm:bidir" + MetaKey_TsunamiClear = "tsunami:*" MetaKey_TsunamiSdkReplacePath = "tsunami:sdkreplacepath" MetaKey_TsunamiAppPath = "tsunami:apppath" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index 2280b55d2d..566d8b2655 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -142,6 +142,9 @@ type MetaTSType struct { MarkdownFontSize float64 `json:"markdown:fontsize,omitempty"` MarkdownFixedFontSize float64 `json:"markdown:fixedfontsize,omitempty"` + PreviewFollowTermId string `json:"preview:followtermid,omitempty"` + PreviewFollowTermBidir *bool `json:"preview:followterm:bidir,omitempty"` + TsunamiClear bool `json:"tsunami:*,omitempty"` TsunamiSdkReplacePath string `json:"tsunami:sdkreplacepath,omitempty"` TsunamiAppPath string `json:"tsunami:apppath,omitempty"` From a28a45eee31ff9500781c90b9d1e11cf2a01d348 Mon Sep 17 00:00:00 2001 From: Cowhen Date: Sat, 18 Apr 2026 22:07:40 +0200 Subject: [PATCH 2/4] Improve block rename and preview follow terminal features Block rename improvements: - Restrict rename menu to terminal blocks only (match badge visibility) - Disable rename for preview/ephemeral blocks - Add race condition protection for Escape/blur handling - Auto-select text on focus for better UX - Await RPC calls and log errors instead of fire-and-forget Preview follow terminal enhancements: - Add keyboard accessibility (Escape, Enter, Space navigation) - Add ARIA roles and labels for screen readers - Implement focus management (capture and restore) - Fix effect dependencies to prevent stale closures - Improve bidir suppression with timeout cleanup - Make sendCdToTerminal shell-aware (POSIX, PowerShell, cmd.exe) - Clear bidir state when linking new terminal - Fix terminal numbering to use tab order instead of filtered index - Move helper functions after imports for better organization Dev build improvements: - Update CLAUDE.md with explicit config/data env variables - Make launch_wave_dev.command portable with dynamic paths - Document why explicit overrides needed for clean installs Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 7 +- frontend/app/block/blockframe-header.tsx | 59 ++++++--- frontend/app/view/preview/preview-model.tsx | 23 ++-- frontend/app/view/preview/preview.tsx | 140 +++++++++++++++++--- launch_wave_dev.command | 7 +- 5 files changed, 191 insertions(+), 45 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 124b47ef5b..e25177f547 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,10 @@ This project uses a set of "skill" guides — focused how-to documents for commo - **Build**: `task package` (requires `PATH="/opt/homebrew/bin:$PATH"` for Go/Task). Builds as `Wave Dev.app` in `make/mac-arm64/`. - **Launch**: Use `launch_wave_dev.command` or run directly: ```bash - WAVETERM_HOME=~/.waveterm-dev make/mac-arm64/Wave\ Dev.app/Contents/MacOS/Wave\ Dev + WAVETERM_HOME=~/.waveterm-dev \ + WAVETERM_CONFIG_HOME=~/.waveterm-dev/config \ + WAVETERM_DATA_HOME=~/.waveterm-dev/data \ + make/mac-arm64/Wave\ Dev.app/Contents/MacOS/Wave\ Dev ``` - `WAVETERM_HOME` gives the dev build a separate data directory (`~/.waveterm-dev`) from the vanilla install (`~/.waveterm`). + These variables create isolated config and data directories for the dev build. Note: `getWaveHomeDir()` only honors `WAVETERM_HOME` after `wave.lock` exists, so explicit `CONFIG/DATA` overrides are needed for clean installs and newly launched dev instances. - **Do not modify Info.plist or re-sign** the built app bundle — it breaks code signing on macOS and causes crashes. diff --git a/frontend/app/block/blockframe-header.tsx b/frontend/app/block/blockframe-header.tsx index 4223397a77..e2fd34dcb0 100644 --- a/frontend/app/block/blockframe-header.tsx +++ b/frontend/app/block/blockframe-header.tsx @@ -37,16 +37,24 @@ function handleHeaderContextMenu( blockId: string, viewModel: ViewModel, nodeModel: NodeModel, - blockEnv: BlockEnv + blockEnv: BlockEnv, + preview: boolean ) { e.preventDefault(); e.stopPropagation(); const magnified = globalStore.get(nodeModel.isMagnified); - const menu: ContextMenuItem[] = [ - { + const ephemeral = globalStore.get(nodeModel.isEphemeral); + const useTermHeader = viewModel?.useTermHeader ? globalStore.get(viewModel.useTermHeader) : false; + const menu: ContextMenuItem[] = []; + + if (!ephemeral && !preview && useTermHeader) { + menu.push({ label: "Rename Block", click: () => startBlockRename(blockId), - }, + }); + } + + menu.push( { label: magnified ? "Un-Magnify Block" : "Magnify Block", click: () => { @@ -59,8 +67,8 @@ function handleHeaderContextMenu( click: () => { navigator.clipboard.writeText(blockId); }, - }, - ]; + } + ); const extraItems = viewModel?.getSettingsMenuItems?.(); if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems); menu.push( @@ -91,14 +99,23 @@ const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: Head const useTermHeader = util.useAtomValueSafe(viewModel?.useTermHeader); let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText); headerTextUnion = frameText ?? headerTextUnion; + const cancelRef = React.useRef(false); const saveRename = React.useCallback( - (newTitle: string) => { + async (newTitle: string) => { + if (cancelRef.current) { + cancelRef.current = false; + return; + } const val = newTitle.trim() || null; - waveEnv.rpc.SetMetaCommand(TabRpcClient, { - oref: WOS.makeORef("block", blockId), - meta: { "frame:title": val }, - }); + try { + await waveEnv.rpc.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", blockId), + meta: { "frame:title": val }, + }); + } catch (error) { + console.error("Failed to save block rename:", error); + } stopBlockRename(); }, [blockId, waveEnv] @@ -112,11 +129,20 @@ const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: Head defaultValue={frameTitle ?? ""} placeholder="Block name..." className="block-frame-rename-input bg-transparent border border-white/20 rounded px-2 py-0.5 text-sm outline-none focus:border-white/40 min-w-0 w-full max-w-[200px]" - onBlur={(e) => saveRename(e.currentTarget.value)} + onFocus={(e) => e.currentTarget.select()} + onBlur={(e) => { + if (cancelRef.current) { + cancelRef.current = false; + stopBlockRename(); + return; + } + saveRename(e.currentTarget.value); + }} onKeyDown={(e) => { if (e.key === "Enter") { saveRename(e.currentTarget.value); } else if (e.key === "Escape") { + cancelRef.current = true; stopBlockRename(); } }} @@ -174,9 +200,10 @@ type HeaderEndIconsProps = { viewModel: ViewModel; nodeModel: NodeModel; blockId: string; + preview: boolean; }; -const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndIconsProps) => { +const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId, preview }: HeaderEndIconsProps) => { const blockEnv = useWaveEnv(); const endIconButtons = util.useAtomValueSafe(viewModel?.endIconButtons); const magnified = jotai.useAtomValue(nodeModel.isMagnified); @@ -226,7 +253,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI elemtype: "iconbutton", icon: "cog", title: "Settings", - click: (e) => handleHeaderContextMenu(e, blockId, viewModel, nodeModel, blockEnv), + click: (e) => handleHeaderContextMenu(e, blockId, viewModel, nodeModel, blockEnv, preview), }; endIconsElem.push(); if (ephemeral) { @@ -309,7 +336,7 @@ const BlockFrame_Header = ({ className={cn("block-frame-default-header", useTermHeader && "!pl-[2px]")} data-role="block-header" ref={dragHandleRef} - onContextMenu={(e) => handleHeaderContextMenu(e, nodeModel.blockId, viewModel, nodeModel, waveEnv)} + onContextMenu={(e) => handleHeaderContextMenu(e, nodeModel.blockId, viewModel, nodeModel, waveEnv, preview)} > {!useTermHeader && ( <> @@ -344,7 +371,7 @@ const BlockFrame_Header = ({
)} - +
); }; diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index 2748c19910..0175d96155 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -520,17 +520,22 @@ export class PreviewModel implements ViewModel { showFollowTermMenu(e: React.MouseEvent) { const tabData = globalStore.get(this.tabModel.tabAtom); const blockIds = tabData?.blockids ?? []; - const terms = blockIds - .filter((bid) => bid !== this.blockId) - .map((bid) => { + + const termBlockIds = blockIds + .filter((bid) => { + if (bid === this.blockId) return false; const block = WOS.getObjectValue(WOS.makeORef("block", bid), globalStore.get); - return { blockId: bid, block }; - }) - .filter(({ block }) => block?.meta?.view === "term") - .map(({ blockId: bid, block }, i) => ({ + return block?.meta?.view === "term"; + }); + + const terms = termBlockIds.map((bid) => { + const block = WOS.getObjectValue(WOS.makeORef("block", bid), globalStore.get); + const termIndex = termBlockIds.indexOf(bid) + 1; + return { blockId: bid, - title: (block?.meta?.["frame:title"] as string) || `Terminal ${i + 1}`, - })); + title: (block?.meta?.["frame:title"] as string) || `Terminal ${termIndex}`, + }; + }); const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); const currentFollowId = globalStore.get(this.followTermIdAtom); diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index b18806f30a..43ca8d57ef 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -4,20 +4,6 @@ import { CenteredDiv } from "@/app/element/quickelems"; import { globalStore } from "@/app/store/jotaiStore"; import { TabRpcClient } from "@/app/store/wshrpcutil"; - -function shellEscapePath(path: string): string { - if (path === "~") return "~"; - if (path.startsWith("~/")) { - // ~ must be unquoted to expand; single-quote the rest - return "~/" + "'" + path.slice(2).replace(/'/g, "'\\''") + "'"; - } - return "'" + path.replace(/'/g, "'\\''") + "'"; -} - -async function sendCdToTerminal(termBlockId: string, path: string, env: import("./previewenv").PreviewEnv) { - const command = "\x15cd " + shellEscapePath(path) + "\r"; - await env.rpc.ControllerInputCommand(TabRpcClient, { blockid: termBlockId, inputdata64: stringToBase64(command) }); -} import { BlockHeaderSuggestionControl } from "@/app/suggestion/suggestion"; import { useWaveEnv } from "@/app/waveenv/waveenv"; import { BlockModel } from "@/app/block/block-model"; @@ -35,6 +21,64 @@ import type { PreviewModel } from "./preview-model"; import { StreamingPreview } from "./preview-streaming"; import type { PreviewEnv } from "./previewenv"; +function posixEscapePath(path: string): string { + if (path === "~") return "~"; + if (path.startsWith("~/")) { + return "~/" + "'" + path.slice(2).replace(/'/g, "'\\''") + "'"; + } + return "'" + path.replace(/'/g, "'\\''") + "'"; +} + +function powershellEscapePath(path: string): string { + if (path === "~") return "~"; + if (path.startsWith("~/")) { + return "~/" + "'" + path.slice(2).replace(/'/g, "''") + "'"; + } + return "'" + path.replace(/'/g, "''") + "'"; +} + +function cmdEscapePath(path: string): string { + if (path.startsWith("~/")) { + return '"%USERPROFILE%\\' + path.slice(2).replace(/\//g, "\\") + '"'; + } + if (path.includes(" ")) { + return '"' + path.replace(/\//g, "\\") + '"'; + } + return path.replace(/\//g, "\\"); +} + +function getShellType(termBlockId: string): string { + const termBlock = WOS.getObjectValue(WOS.makeORef("block", termBlockId), globalStore.get); + const shellPath = (termBlock?.meta?.["cmd:shell"] as string) ?? ""; + if (shellPath.includes("powershell") || shellPath.includes("pwsh")) { + return "powershell"; + } + if (shellPath.includes("cmd.exe")) { + return "cmd"; + } + return "posix"; +} + +async function sendCdToTerminal(termBlockId: string, path: string, env: import("./previewenv").PreviewEnv) { + const shellType = getShellType(termBlockId); + let command: string; + + switch (shellType) { + case "powershell": + command = "cd " + powershellEscapePath(path) + "\r"; + break; + case "cmd": + command = "cd " + cmdEscapePath(path) + "\r"; + break; + case "posix": + default: + command = "\x15cd " + posixEscapePath(path) + "\r"; + break; + } + + await env.rpc.ControllerInputCommand(TabRpcClient, { blockid: termBlockId, inputdata64: stringToBase64(command) }); +} + export type SpecializedViewProps = { model: PreviewModel; parentRef: React.RefObject; @@ -115,6 +159,8 @@ const fetchSuggestions = async ( function FollowTermDropdown({ model }: { model: PreviewModel }) { const menuData = useAtomValue(model.followTermMenuDataAtom); + const menuRef = useRef(null); + const previousActiveElement = useRef(null); if (!menuData) return null; @@ -122,15 +168,22 @@ function FollowTermDropdown({ model }: { model: PreviewModel }) { const closeMenu = () => { BlockModel.getInstance().setBlockHighlight(null); globalStore.set(model.followTermMenuDataAtom, null); + if (previousActiveElement.current instanceof HTMLElement) { + previousActiveElement.current.focus(); + } }; const linkTerm = (blockId: string) => { BlockModel.getInstance().setBlockHighlight(null); fireAndForget(async () => { await model.env.services.object.UpdateObjectMeta(WOS.makeORef("block", model.blockId), { "preview:followtermid": blockId, + "preview:followterm:bidir": false, }); }); globalStore.set(model.followTermMenuDataAtom, null); + if (previousActiveElement.current instanceof HTMLElement) { + previousActiveElement.current.focus(); + } }; const toggleBidir = () => { fireAndForget(async () => { @@ -148,8 +201,30 @@ function FollowTermDropdown({ model }: { model: PreviewModel }) { }); }); globalStore.set(model.followTermMenuDataAtom, null); + if (previousActiveElement.current instanceof HTMLElement) { + previousActiveElement.current.focus(); + } }; + useEffect(() => { + previousActiveElement.current = document.activeElement; + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") { + closeMenu(); + } + }; + document.addEventListener("keydown", handleEscape); + if (menuRef.current) { + const firstItem = menuRef.current.querySelector('[role="menuitem"]'); + if (firstItem instanceof HTMLElement) { + firstItem.focus(); + } + } + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, []); + const dropdownStyle: React.CSSProperties = { left: pos.x, top: pos.y, @@ -163,13 +238,16 @@ function FollowTermDropdown({ model }: { model: PreviewModel }) { return ReactDOM.createPortal( <>
-
+
{terms.length === 0 ? (
No terminals on this tab
) : ( terms.map(({ blockId, title }) => (
BlockModel.getInstance().setBlockHighlight({ blockId, icon: "terminal" }) @@ -177,6 +255,12 @@ function FollowTermDropdown({ model }: { model: PreviewModel }) { onMouseLeave={() => BlockModel.getInstance().setBlockHighlight(null)} onMouseDown={(e) => e.stopPropagation()} onClick={() => linkTerm(blockId)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + linkTerm(blockId); + } + }} > {title} @@ -187,9 +271,18 @@ function FollowTermDropdown({ model }: { model: PreviewModel }) { <>
e.stopPropagation()} onClick={toggleBidir} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggleBidir(); + } + }} >
e.stopPropagation()} onClick={unlink} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + unlink(); + } + }} > Stop Following
@@ -250,7 +352,11 @@ function PreviewView({ if (!followTermId || !followTermCwd) return; suppressBidirRef.current = true; fireAndForget(() => model.goHistory(followTermCwd)); - }, [followTermCwd]); + const timer = setTimeout(() => { + suppressBidirRef.current = false; + }, 100); + return () => clearTimeout(timer); + }, [followTermCwd, followTermId, model]); useEffect(() => { if (!followTermId || !followTermBidir) return; @@ -262,7 +368,7 @@ function PreviewView({ const fi = loadableFileInfo.data; if (!fi || fi.mimetype !== "directory" || !fi.path) return; fireAndForget(() => sendCdToTerminal(followTermId, fi.path, env)); - }, [loadableFileInfo]); + }, [loadableFileInfo, followTermId, followTermBidir, env]); if (connStatus?.status != "connected") { return null; diff --git a/launch_wave_dev.command b/launch_wave_dev.command index cf3e85d1e5..dd3d1b0af9 100755 --- a/launch_wave_dev.command +++ b/launch_wave_dev.command @@ -1,4 +1,9 @@ #!/bin/bash # Launch Wave (dev build) with a separate data directory # Double-click this file in Finder to launch, or run from terminal. -WAVETERM_HOME="$HOME/.waveterm-dev" exec /Users/zmd2hi/Documents/00_personal/wave_term/make/mac-arm64/Wave\ Dev.app/Contents/MacOS/Wave\ Dev +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WAVETERM_HOME="$SCRIPT_DIR/.waveterm-dev" +WAVETERM_CONFIG_HOME="$WAVETERM_HOME/config" +WAVETERM_DATA_HOME="$WAVETERM_HOME/data" +export WAVETERM_HOME WAVETERM_CONFIG_HOME WAVETERM_DATA_HOME +exec "$SCRIPT_DIR/make/mac-arm64/Wave Dev.app/Contents/MacOS/Wave Dev" From df8483a183fe0c3a36bfe419af8d21b5bfdc37cd Mon Sep 17 00:00:00 2001 From: Cowhen Date: Sun, 19 Apr 2026 10:10:38 +0200 Subject: [PATCH 3/4] Fix block rename error handling and type annotation - Move stopBlockRename() to only execute after successful RPC - Keep rename input open on error so user can retry - Add explicit type cast for followTermMenuDataAtom to fix type inference Co-Authored-By: Claude Sonnet 4.5 --- frontend/app/block/blockframe-header.tsx | 2 +- frontend/app/view/preview/preview-model.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/block/blockframe-header.tsx b/frontend/app/block/blockframe-header.tsx index e2fd34dcb0..90dd0e19a1 100644 --- a/frontend/app/block/blockframe-header.tsx +++ b/frontend/app/block/blockframe-header.tsx @@ -113,10 +113,10 @@ const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: Head oref: WOS.makeORef("block", blockId), meta: { "frame:title": val }, }); + stopBlockRename(); } catch (error) { console.error("Failed to save block rename:", error); } - stopBlockRename(); }, [blockId, waveEnv] ); diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index 0175d96155..5dae88b31a 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -514,7 +514,7 @@ export class PreviewModel implements ViewModel { this.followTermBidirAtom = atom((get) => { return (get(this.blockAtom)?.meta?.["preview:followterm:bidir"] as boolean) ?? false; }); - this.followTermMenuDataAtom = atom(null); + this.followTermMenuDataAtom = atom(null) as PrimitiveAtom<{ pos: any; terms: any; currentFollowId: any; bidir: any } | null>; } showFollowTermMenu(e: React.MouseEvent) { From dfb9c4bbad4ffd5a6cd7233f510f7ae1b3cc587c Mon Sep 17 00:00:00 2001 From: Cowhen Date: Sun, 19 Apr 2026 20:52:55 +0200 Subject: [PATCH 4/4] Fix critical security and correctness issues in preview follow terminal **P0 Fixes:** - Fix React hooks violation: move useEffect before conditional return in FollowTermDropdown - Remove broken shell type detection (cmd:shell is boolean, not shell path) **P1 Fixes:** - Fix command injection by removing vulnerable cmdEscapePath/powershellEscapePath - Revert to POSIX-only shell escaping (safe for bash/zsh/sh) - Fix race condition: remove timeout-based suppressBidir reset **P2 Fixes:** - Fix stale closure: make closeMenu a useCallback with proper dependencies - Fix linkTerm behavior: only reset bidir when linking to different terminal - Improve type safety: use proper types instead of any in followTermMenuDataAtom Co-Authored-By: Claude Sonnet 4.5 --- frontend/app/view/preview/preview-model.tsx | 28 +++-- frontend/app/view/preview/preview.tsx | 114 ++++++-------------- 2 files changed, 47 insertions(+), 95 deletions(-) diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index 5dae88b31a..18f2bc9c53 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -514,28 +514,26 @@ export class PreviewModel implements ViewModel { this.followTermBidirAtom = atom((get) => { return (get(this.blockAtom)?.meta?.["preview:followterm:bidir"] as boolean) ?? false; }); - this.followTermMenuDataAtom = atom(null) as PrimitiveAtom<{ pos: any; terms: any; currentFollowId: any; bidir: any } | null>; + this.followTermMenuDataAtom = atom(null) as PrimitiveAtom<{ pos: { x: number; y: number }; terms: { blockId: string; title: string }[]; currentFollowId: string | null; bidir: boolean } | null>; } showFollowTermMenu(e: React.MouseEvent) { const tabData = globalStore.get(this.tabModel.tabAtom); const blockIds = tabData?.blockids ?? []; - const termBlockIds = blockIds - .filter((bid) => { - if (bid === this.blockId) return false; - const block = WOS.getObjectValue(WOS.makeORef("block", bid), globalStore.get); - return block?.meta?.view === "term"; - }); - - const terms = termBlockIds.map((bid) => { + const terms: { blockId: string; title: string }[] = []; + let termIndex = 1; + for (const bid of blockIds) { + if (bid === this.blockId) continue; const block = WOS.getObjectValue(WOS.makeORef("block", bid), globalStore.get); - const termIndex = termBlockIds.indexOf(bid) + 1; - return { - blockId: bid, - title: (block?.meta?.["frame:title"] as string) || `Terminal ${termIndex}`, - }; - }); + if (block?.meta?.view === "term") { + terms.push({ + blockId: bid, + title: (block?.meta?.["frame:title"] as string) || `Terminal ${termIndex}`, + }); + termIndex++; + } + } const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); const currentFollowId = globalStore.get(this.followTermIdAtom); diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 43ca8d57ef..e02a367ee4 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -10,7 +10,7 @@ import { BlockModel } from "@/app/block/block-model"; import * as WOS from "@/store/wos"; import { fireAndForget, isBlank, makeConnRoute, stringToBase64 } from "@/util/util"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { memo, useEffect, useRef } from "react"; +import React, { memo, useEffect, useRef } from "react"; import ReactDOM from "react-dom"; import { CSVView } from "./csvview"; import { DirectoryPreview } from "./preview-directory"; @@ -21,7 +21,7 @@ import type { PreviewModel } from "./preview-model"; import { StreamingPreview } from "./preview-streaming"; import type { PreviewEnv } from "./previewenv"; -function posixEscapePath(path: string): string { +function shellEscapePath(path: string): string { if (path === "~") return "~"; if (path.startsWith("~/")) { return "~/" + "'" + path.slice(2).replace(/'/g, "'\\''") + "'"; @@ -29,53 +29,8 @@ function posixEscapePath(path: string): string { return "'" + path.replace(/'/g, "'\\''") + "'"; } -function powershellEscapePath(path: string): string { - if (path === "~") return "~"; - if (path.startsWith("~/")) { - return "~/" + "'" + path.slice(2).replace(/'/g, "''") + "'"; - } - return "'" + path.replace(/'/g, "''") + "'"; -} - -function cmdEscapePath(path: string): string { - if (path.startsWith("~/")) { - return '"%USERPROFILE%\\' + path.slice(2).replace(/\//g, "\\") + '"'; - } - if (path.includes(" ")) { - return '"' + path.replace(/\//g, "\\") + '"'; - } - return path.replace(/\//g, "\\"); -} - -function getShellType(termBlockId: string): string { - const termBlock = WOS.getObjectValue(WOS.makeORef("block", termBlockId), globalStore.get); - const shellPath = (termBlock?.meta?.["cmd:shell"] as string) ?? ""; - if (shellPath.includes("powershell") || shellPath.includes("pwsh")) { - return "powershell"; - } - if (shellPath.includes("cmd.exe")) { - return "cmd"; - } - return "posix"; -} - async function sendCdToTerminal(termBlockId: string, path: string, env: import("./previewenv").PreviewEnv) { - const shellType = getShellType(termBlockId); - let command: string; - - switch (shellType) { - case "powershell": - command = "cd " + powershellEscapePath(path) + "\r"; - break; - case "cmd": - command = "cd " + cmdEscapePath(path) + "\r"; - break; - case "posix": - default: - command = "\x15cd " + posixEscapePath(path) + "\r"; - break; - } - + const command = "\x15cd " + shellEscapePath(path) + "\r"; await env.rpc.ControllerInputCommand(TabRpcClient, { blockid: termBlockId, inputdata64: stringToBase64(command) }); } @@ -162,23 +117,45 @@ function FollowTermDropdown({ model }: { model: PreviewModel }) { const menuRef = useRef(null); const previousActiveElement = useRef(null); - if (!menuData) return null; - - const { pos, terms, currentFollowId, bidir } = menuData; - const closeMenu = () => { + const closeMenu = React.useCallback(() => { BlockModel.getInstance().setBlockHighlight(null); globalStore.set(model.followTermMenuDataAtom, null); if (previousActiveElement.current instanceof HTMLElement) { previousActiveElement.current.focus(); } - }; + }, [model.followTermMenuDataAtom]); + + useEffect(() => { + if (!menuData) return; + previousActiveElement.current = document.activeElement; + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") { + closeMenu(); + } + }; + document.addEventListener("keydown", handleEscape); + if (menuRef.current) { + const firstItem = menuRef.current.querySelector('[role="menuitem"]'); + if (firstItem instanceof HTMLElement) { + firstItem.focus(); + } + } + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, [menuData, closeMenu]); + + if (!menuData) return null; + + const { pos, terms, currentFollowId, bidir } = menuData; const linkTerm = (blockId: string) => { BlockModel.getInstance().setBlockHighlight(null); fireAndForget(async () => { - await model.env.services.object.UpdateObjectMeta(WOS.makeORef("block", model.blockId), { - "preview:followtermid": blockId, - "preview:followterm:bidir": false, - }); + const updates: Record = { "preview:followtermid": blockId }; + if (blockId !== currentFollowId) { + updates["preview:followterm:bidir"] = false; + } + await model.env.services.object.UpdateObjectMeta(WOS.makeORef("block", model.blockId), updates); }); globalStore.set(model.followTermMenuDataAtom, null); if (previousActiveElement.current instanceof HTMLElement) { @@ -206,25 +183,6 @@ function FollowTermDropdown({ model }: { model: PreviewModel }) { } }; - useEffect(() => { - previousActiveElement.current = document.activeElement; - const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape") { - closeMenu(); - } - }; - document.addEventListener("keydown", handleEscape); - if (menuRef.current) { - const firstItem = menuRef.current.querySelector('[role="menuitem"]'); - if (firstItem instanceof HTMLElement) { - firstItem.focus(); - } - } - return () => { - document.removeEventListener("keydown", handleEscape); - }; - }, []); - const dropdownStyle: React.CSSProperties = { left: pos.x, top: pos.y, @@ -352,10 +310,6 @@ function PreviewView({ if (!followTermId || !followTermCwd) return; suppressBidirRef.current = true; fireAndForget(() => model.goHistory(followTermCwd)); - const timer = setTimeout(() => { - suppressBidirRef.current = false; - }, 100); - return () => clearTimeout(timer); }, [followTermCwd, followTermId, model]); useEffect(() => {