diff --git a/.github/workflows/merge-gatekeeper.yml b/.github/workflows/merge-gatekeeper.yml index 7a8953cdf..a433e4c88 100644 --- a/.github/workflows/merge-gatekeeper.yml +++ b/.github/workflows/merge-gatekeeper.yml @@ -23,4 +23,4 @@ jobs: uses: upsidr/merge-gatekeeper@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - ignored: Test Onboarding, Analyze (go), Analyze (javascript-typescript), License Compliance + ignored: Test Onboarding, Analyze (go), Analyze (javascript-typescript), License Compliance, CodeRabbit diff --git a/cmd/wsh/cmd/wshcmd-shell-unix.go b/cmd/wsh/cmd/wshcmd-shell-unix.go index dfe044aeb..b3b85b446 100644 --- a/cmd/wsh/cmd/wshcmd-shell-unix.go +++ b/cmd/wsh/cmd/wshcmd-shell-unix.go @@ -58,5 +58,5 @@ func shellCmdInner() string { } } // none found - return "bin/bash\n" + return "/bin/bash\n" } diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx index c5c356d27..1a7d352d7 100644 --- a/docs/docs/config.mdx +++ b/docs/docs/config.mdx @@ -45,6 +45,8 @@ wsh editconfig | term:localshellopts | string[] | set to pass additional parameters to the term:localshellpath (example: `["-NoLogo"]` for PowerShell will remove the copyright notice) | | term:copyonselect | bool | set to false to disable terminal copy-on-select | | term:scrollback | int | size of terminal scrollback buffer, max is 10000 | +| term:theme | string | preset name of terminal theme to apply by default (default is "default-dark") | +| term:transparency | float64 | set the background transparency of terminal theme (default 0.5, 0 = not transparent, 1.0 = fully transparent) | | editor:minimapenabled | bool | set to false to disable editor minimap | | editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false | | editor:wordwrap | bool | set to true to enable word wrapping in the editor (defaults to false) | @@ -75,9 +77,11 @@ wsh editconfig | window:autohidetabbar | bool | show and hide the tab bar automatically when the mouse moves near the top of the window | window:nativetitlebar | bool | set to use the OS-native title bar, rather than the overlay (Windows and Linux only, requires app restart) | | window:disablehardwareacceleration | bool | set to disable Chromium hardware acceleration to resolve graphical bugs (requires app restart) | +| window:savelastwindow | bool | when `true`, the last window that is closed is preserved and is reopened the next time the app is launched (defaults to `true`) | +| window:confirmonclose | bool | when `true`, a prompt will ask a user to confirm that they want to close a window if it has an unsaved workspace with more than one tab (defaults to `true`) | | telemetry:enabled | bool | set to enable/disable telemetry | -For reference this is the current default configuration (v0.9.3): +For reference, this is the current default configuration (v0.10.4): ```json { @@ -89,6 +93,7 @@ For reference this is the current default configuration (v0.9.3): "autoupdate:installonquit": true, "autoupdate:intervalms": 3600000, "conn:askbeforewshinstall": true, + "conn:wshenabled": true, "editor:minimapenabled": true, "web:defaulturl": "https://github.com/wavetermdev/waveterm", "web:defaultsearch": "https://www.google.com/search?q={query}", @@ -99,6 +104,8 @@ For reference this is the current default configuration (v0.9.3): "window:magnifiedblocksize": 0.9, "window:magnifiedblockblurprimarypx": 10, "window:magnifiedblockblursecondarypx": 2, + "window:confirmclose": true, + "window:savelastwindow": true, "telemetry:enabled": true, "term:copyonselect": true } diff --git a/docs/docs/keybindings.mdx b/docs/docs/keybindings.mdx index cfb1569e5..7bd2775d6 100644 --- a/docs/docs/keybindings.mdx +++ b/docs/docs/keybindings.mdx @@ -71,6 +71,14 @@ replace "Cmd" with "Alt" (note that "Ctrl" is "Ctrl" on both Mac, Windows, and L | ---------------- | ------------- | | | Clear AI Chat | +## Terminal Keybindings + +| Key | Function | +| ----------------------- | -------------- | +| | Copy | +| | Paste | +| | Clear Terminal | + ## Customizeable Systemwide Global Hotkey Wave allows setting a custom global hotkey to focus your most recent window from anywhere in your computer. For more information on this, see [the config docs](./config#customizable-systemwide-global-hotkey). diff --git a/docs/docs/releasenotes.mdx b/docs/docs/releasenotes.mdx index 1f4e8bed2..fb44f2746 100644 --- a/docs/docs/releasenotes.mdx +++ b/docs/docs/releasenotes.mdx @@ -6,6 +6,36 @@ sidebar_position: 200 # Release Notes +### v0.10.4 — Dec 20, 2024 + +Quick update with bug fixes and new configuration options + +- Added "window:confirmclose" and "window:savelastwindow" configuration options +- [bugfix] Fixed broken scroll bar in the AI widget +- [bugfix] Fixed default path for wsh shell detection (used in remote connections) +- Dependency updates + +### v0.10.3 — Dec 19, 2024 + +Quick update to v0.10 with new features and bug fixes. + +- Global hotkey support [docs](https://docs.waveterm.dev/config#customizable-systemwide-global-hotkey) +- Added configuration to override the font size for markdown, AI-chat, and preview editor [docs](https://docs.waveterm.dev/config) +- Added ability to set independent zoom level for the web view (right click block header) +- New `wsh wavepath` command to open the config directory, data directory, and log file +- [bugfix] Fixed crash when /etc/sshd_config contained an unsupported Match directive (most common on Fedora) +- [bugfix] Workspaces are now more consistent across windows, closes associated window when Workspaces are deleted +- [bugfix] Fixed zsh on WSL +- [bugfix] Fixed long-standing bug around control sequences sometimes showing up in terminal output when switching tabs +- Lots of new examples in the docs for shell overrides, presets, widgets, and connections +- Other bug fixes and UI updates + +(note, v0.10.2 and v0.10.3's release notes have been merged together) + +### v0.10.1 — Dec 12, 2024 + +Quick update to fix the workspace app menu actions. Also fixes workspace switching to always open a new window when invoked from a non-workspace window. This reduces the chance of losing a non-workspace window's tabs accidentally. + ### v0.10.0 — Dec 11, 2024 Wave Terminal v0.10.0 introduces workspaces, making it easier to manage multiple work environments. We've added powerful new command execution capabilities with `wsh run`, allowing you to launch and control commands in dedicated blocks. This release also brings significant improvements to SSH with a new connections configuration system for managing your remote environments. diff --git a/docs/src/components/kbd.tsx b/docs/src/components/kbd.tsx index 69fee9ebf..d8550521a 100644 --- a/docs/src/components/kbd.tsx +++ b/docs/src/components/kbd.tsx @@ -38,9 +38,9 @@ function convertKey(platform: Platform, key: string): [any, string, boolean] { return ["⇧", "Shift", true]; } if (key == "Escape") { - return ["Esc", null, false]; + return ["Esc", "Escape", false]; } - return [key, null, false]; + return [key.length > 1 ? key : key.toUpperCase(), key, false]; } // Custom KBD component @@ -50,7 +50,7 @@ const KbdInternal = ({ k }: { k: string }) => { const keyElems = keys.map((key, i) => { const [displayKey, title, symbol] = convertKey(platform, key); return ( - + {displayKey} ); diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 7cc076ad5..068703a70 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -229,21 +229,26 @@ export class WaveBrowserWindow extends BaseWindow { e.preventDefault(); fireAndForget(async () => { const numWindows = waveWindowMap.size; - if (numWindows > 1) { - console.log("numWindows > 1", numWindows); - const workspace = await WorkspaceService.GetWorkspace(this.workspaceId); - console.log("workspace", workspace); - if (isNonEmptyUnsavedWorkspace(workspace)) { - console.log("workspace has no name, icon, and multiple tabs", workspace); - const choice = dialog.showMessageBoxSync(this, { - type: "question", - buttons: ["Cancel", "Close Window"], - title: "Confirm", - message: "Window has unsaved tabs, closing window will delete existing tabs.\n\nContinue?", - }); - if (choice === 0) { - console.log("user cancelled close window", this.waveWindowId); - return; + const fullConfig = await FileService.GetFullConfig(); + if (numWindows > 1 || !fullConfig.settings["window:savelastwindow"]) { + console.log("numWindows > 1 or user does not want last window saved", numWindows); + if (fullConfig.settings["window:confirmclose"]) { + console.log("confirmclose", this.waveWindowId); + const workspace = await WorkspaceService.GetWorkspace(this.workspaceId); + console.log("workspace", workspace); + if (isNonEmptyUnsavedWorkspace(workspace)) { + console.log("workspace has no name, icon, and multiple tabs", workspace); + const choice = dialog.showMessageBoxSync(this, { + type: "question", + buttons: ["Cancel", "Close Window"], + title: "Confirm", + message: + "Window has unsaved tabs, closing window will delete existing tabs.\n\nContinue?", + }); + if (choice === 0) { + console.log("user cancelled close window", this.waveWindowId); + return; + } } } console.log("deleteAllowed = true", this.waveWindowId); @@ -270,11 +275,6 @@ export class WaveBrowserWindow extends BaseWindow { this.destroy(); return; } - const numWindows = waveWindowMap.size; - if (numWindows == 0) { - console.log("win no windows left", this.waveWindowId); - return; - } if (this.deleteAllowed) { console.log("win removing window from backend DB", this.waveWindowId); fireAndForget(() => WindowService.CloseWindow(this.waveWindowId, true)); diff --git a/frontend/app/element/markdown.scss b/frontend/app/element/markdown.scss index 4bee56855..b49cdccfd 100644 --- a/frontend/app/element/markdown.scss +++ b/frontend/app/element/markdown.scss @@ -24,6 +24,10 @@ overflow: hidden; } + *:last-child { + margin-bottom: 0 !important; + } + .heading:not(.heading ~ .heading) { margin-top: 0 !important; } diff --git a/frontend/app/element/markdown.tsx b/frontend/app/element/markdown.tsx index 5e6f596cf..a59fa877a 100644 --- a/frontend/app/element/markdown.tsx +++ b/frontend/app/element/markdown.tsx @@ -9,7 +9,7 @@ import { resolveSrcSet, transformBlocks, } from "@/app/element/markdown-util"; -import { useAtomValueSafe } from "@/util/util"; +import { boundNumber, useAtomValueSafe } from "@/util/util"; import { clsx } from "clsx"; import { Atom } from "jotai"; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; @@ -380,10 +380,10 @@ const Markdown = ({ const mergedStyle = { ...style }; if (fontSizeOverride != null) { - mergedStyle["--markdown-font-size"] = `${fontSizeOverride}px`; + mergedStyle["--markdown-font-size"] = `${boundNumber(fontSizeOverride, 6, 64)}px`; } if (fixedFontSizeOverride != null) { - mergedStyle["--markdown-fixed-font-size"] = `${fixedFontSizeOverride}px`; + mergedStyle["--markdown-fixed-font-size"] = `${boundNumber(fixedFontSizeOverride, 6, 64)}px`; } return (
diff --git a/frontend/app/element/search.scss b/frontend/app/element/search.scss new file mode 100644 index 000000000..6a1f68f9b --- /dev/null +++ b/frontend/app/element/search.scss @@ -0,0 +1,43 @@ +.search-container { + display: flex; + flex-direction: row; + background-color: var(--modal-bg-color); + border: 1px solid var(--accent-color); + border-radius: var(--modal-border-radius); + box-shadow: var(--modal-box-shadow); + color: var(--main-text-color); + padding: 5px 5px 5px 10px; + gap: 5px; + width: 50%; + max-width: 300px; + min-width: 200px; + + input { + flex: 1 1 auto; + border: none; + font-size: 14px; + height: 100%; + padding: 0; + border-radius: 0; + } + + .search-results { + font-size: 12px; + margin: auto 0; + color: var(--secondary-text-color); + + &.hidden { + display: none; + } + } + + .right-buttons { + display: flex; + gap: 5px; + padding-left: 5px; + border-left: 1px solid var(--modal-border-color); + button { + font-size: 12px; + } + } +} diff --git a/frontend/app/element/search.stories.tsx b/frontend/app/element/search.stories.tsx new file mode 100644 index 000000000..c44fd54d6 --- /dev/null +++ b/frontend/app/element/search.stories.tsx @@ -0,0 +1,127 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import type { Meta, StoryObj } from "@storybook/react"; +import { useSetAtom } from "jotai"; +import { useEffect } from "react"; +import { Popover } from "./popover"; +import { Search, useSearch } from "./search"; + +const meta: Meta = { + title: "Elements/Search", + component: Search, + args: {}, +}; + +export default meta; +type Story = StoryObj; + +export const DefaultSearch: Story = { + render: (args) => { + const props = useSearch(); + const setIsOpen = useSetAtom(props.isOpenAtom); + useEffect(() => { + setIsOpen(true); + }, []); + return ( +
} + style={{ + border: "2px solid black", + width: "100%", + height: "200px", + background: "var(--main-bg-color)", + }} + > + +
+ ); + }, + args: {}, +}; + +export const Results10: Story = { + render: (args) => { + const props = useSearch(); + const setIsOpen = useSetAtom(props.isOpenAtom); + const setNumResults = useSetAtom(props.numResultsAtom); + useEffect(() => { + setIsOpen(true); + setNumResults(10); + }, []); + return ( +
} + style={{ + border: "2px solid black", + width: "100%", + height: "200px", + background: "var(--main-bg-color)", + }} + > + +
+ ); + }, + args: {}, +}; + +export const InputAndResults10: Story = { + render: (args) => { + const props = useSearch(); + const setIsOpen = useSetAtom(props.isOpenAtom); + const setNumResults = useSetAtom(props.numResultsAtom); + const setSearch = useSetAtom(props.searchAtom); + useEffect(() => { + setIsOpen(true); + setNumResults(10); + setSearch("search term"); + }, []); + return ( +
} + style={{ + border: "2px solid black", + width: "100%", + height: "200px", + background: "var(--main-bg-color)", + }} + > + +
+ ); + }, + args: {}, +}; + +export const LongInputAndResults10: Story = { + render: (args) => { + const props = useSearch(); + const setIsOpen = useSetAtom(props.isOpenAtom); + const setNumResults = useSetAtom(props.numResultsAtom); + const setSearch = useSetAtom(props.searchAtom); + useEffect(() => { + setIsOpen(true); + setNumResults(10); + setSearch("search term ".repeat(10).trimEnd()); + }, []); + return ( +
} + style={{ + border: "2px solid black", + width: "100%", + height: "200px", + background: "var(--main-bg-color)", + }} + > + +
+ ); + }, + args: {}, +}; diff --git a/frontend/app/element/search.tsx b/frontend/app/element/search.tsx new file mode 100644 index 000000000..8a18e753c --- /dev/null +++ b/frontend/app/element/search.tsx @@ -0,0 +1,115 @@ +import { autoUpdate, FloatingPortal, Middleware, offset, useDismiss, useFloating } from "@floating-ui/react"; +import clsx from "clsx"; +import { atom, PrimitiveAtom, useAtom, useAtomValue } from "jotai"; +import { memo, useCallback, useRef, useState } from "react"; +import { IconButton } from "./iconbutton"; +import { Input } from "./input"; +import "./search.scss"; + +type SearchProps = { + searchAtom: PrimitiveAtom; + indexAtom: PrimitiveAtom; + numResultsAtom: PrimitiveAtom; + isOpenAtom: PrimitiveAtom; + anchorRef?: React.RefObject; + offsetX?: number; + offsetY?: number; +}; + +const SearchComponent = ({ + searchAtom, + indexAtom, + numResultsAtom, + isOpenAtom, + anchorRef, + offsetX = 10, + offsetY = 10, +}: SearchProps) => { + const [isOpen, setIsOpen] = useAtom(isOpenAtom); + const [search, setSearch] = useAtom(searchAtom); + const [index, setIndex] = useAtom(indexAtom); + const numResults = useAtomValue(numResultsAtom); + + const handleOpenChange = useCallback((open: boolean) => { + setIsOpen(open); + }, []); + + const middleware: Middleware[] = []; + middleware.push( + offset(({ rects }) => ({ + mainAxis: -rects.floating.height - offsetY, + crossAxis: -offsetX, + })) + ); + + const { refs, floatingStyles, context } = useFloating({ + placement: "top-end", + open: isOpen, + onOpenChange: handleOpenChange, + whileElementsMounted: autoUpdate, + middleware, + elements: { + reference: anchorRef!.current, + }, + }); + + const dismiss = useDismiss(context); + + const prevDecl: IconButtonDecl = { + elemtype: "iconbutton", + icon: "chevron-up", + title: "Previous Result", + disabled: index === 0, + click: () => setIndex(index - 1), + }; + + const nextDecl: IconButtonDecl = { + elemtype: "iconbutton", + icon: "chevron-down", + title: "Next Result", + disabled: !numResults || index === numResults - 1, + click: () => setIndex(index + 1), + }; + + const closeDecl: IconButtonDecl = { + elemtype: "iconbutton", + icon: "xmark-large", + title: "Close", + click: () => setIsOpen(false), + }; + + return ( + <> + {isOpen && ( + +
+ +
+ {index + 1}/{numResults} +
+
+ + + +
+
+
+ )} + + ); +}; + +export const Search = memo(SearchComponent) as typeof SearchComponent; + +export function useSearch(anchorRef?: React.RefObject): SearchProps { + const [searchAtom] = useState(atom("")); + const [indexAtom] = useState(atom(0)); + const [numResultsAtom] = useState(atom(0)); + const [isOpenAtom] = useState(atom(false)); + anchorRef ??= useRef(null); + return { searchAtom, indexAtom, numResultsAtom, isOpenAtom, anchorRef }; +} diff --git a/frontend/app/element/searchinput.scss b/frontend/app/element/searchinput.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/app/element/searchinput.stories.tsx b/frontend/app/element/searchinput.stories.tsx deleted file mode 100644 index 359b0d8e3..000000000 --- a/frontend/app/element/searchinput.stories.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import type { Meta, StoryObj } from "@storybook/react"; -import { SearchInput } from "./searchinput"; - -const meta: Meta = { - title: "Elements/SearchInput", - component: SearchInput, - argTypes: { - className: { - description: "Custom class for styling the input group", - control: { type: "text" }, - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const DefaultSearchInput: Story = { - render: (args) => { - const handleSearch = () => { - console.log("Search triggered"); - }; - - return ; - }, - args: { - className: "custom-search-input", - }, -}; diff --git a/frontend/app/element/searchinput.tsx b/frontend/app/element/searchinput.tsx deleted file mode 100644 index b977a5197..000000000 --- a/frontend/app/element/searchinput.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { Button } from "./button"; -import { Input, InputGroup, InputRightElement } from "./input"; - -const SearchInput = () => { - return ( - - - - - - - ); -}; - -export { SearchInput }; diff --git a/frontend/app/modals/tos.scss b/frontend/app/modals/tos.scss index 72d8015af..1db743d36 100644 --- a/frontend/app/modals/tos.scss +++ b/frontend/app/modals/tos.scss @@ -230,6 +230,7 @@ justify-content: center; button { + padding: 8px 20px; font-size: 14px; } diff --git a/frontend/app/tab/workspaceswitcher.tsx b/frontend/app/tab/workspaceswitcher.tsx index bd995a611..018a9e50c 100644 --- a/frontend/app/tab/workspaceswitcher.tsx +++ b/frontend/app/tab/workspaceswitcher.tsx @@ -153,6 +153,7 @@ const WorkspaceSwitcherItem = ({ const isCurrentWorkspace = activeWorkspace.oid === workspace.oid; const setWorkspace = useCallback((newWorkspace: Workspace) => { + setWorkspaceEntry({ ...workspaceEntry, workspace: newWorkspace }); if (newWorkspace.name != "") { fireAndForget(() => WorkspaceService.UpdateWorkspace( diff --git a/frontend/app/theme.scss b/frontend/app/theme.scss index 9f25430c4..ac5d1cf85 100644 --- a/frontend/app/theme.scss +++ b/frontend/app/theme.scss @@ -83,8 +83,10 @@ --modal-bg-color: #232323; --modal-header-bottom-border-color: rgba(241, 246, 243, 0.15); --modal-border-color: rgba(255, 255, 255, 0.12); /* toggle colors */ + --modal-border-radius: 6px; --toggle-bg-color: var(--border-color); --modal-shadow-color: rgba(0, 0, 0, 0.8); + --modal-box-shadow: box-shadow: 0px 8px 24px 0px var(--modal-shadow-color); --toggle-thumb-color: var(--main-text-color); --toggle-checked-bg-color: var(--accent-color); diff --git a/frontend/app/view/codeeditor/codeeditor.tsx b/frontend/app/view/codeeditor/codeeditor.tsx index aca418143..6eafb2b94 100644 --- a/frontend/app/view/codeeditor/codeeditor.tsx +++ b/frontend/app/view/codeeditor/codeeditor.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { useOverrideConfigAtom } from "@/app/store/global"; +import { boundNumber } from "@/util/util"; import loader from "@monaco-editor/loader"; import { Editor, Monaco } from "@monaco-editor/react"; import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api"; @@ -122,7 +123,7 @@ export function CodeEditor({ blockId, text, language, filename, meta, onChange, const minimapEnabled = useOverrideConfigAtom(blockId, "editor:minimapenabled") ?? false; const stickyScrollEnabled = useOverrideConfigAtom(blockId, "editor:stickyscrollenabled") ?? false; const wordWrap = useOverrideConfigAtom(blockId, "editor:wordwrap") ?? false; - const fontSize = useOverrideConfigAtom(blockId, "editor:fontsize"); + const fontSize = boundNumber(useOverrideConfigAtom(blockId, "editor:fontsize"), 6, 64); const theme = "wave-theme-dark"; React.useEffect(() => { diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index a834adfd5..d6ad8eec9 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -24,6 +24,7 @@ import { } from "@/store/global"; import * as services from "@/store/services"; import * as keyutil from "@/util/keyutil"; +import { boundNumber } from "@/util/util"; import clsx from "clsx"; import debug from "debug"; import * as jotai from "jotai"; @@ -62,6 +63,7 @@ class TermViewModel implements ViewModel { vdomToolbarTarget: jotai.PrimitiveAtom; fontSizeAtom: jotai.Atom; termThemeNameAtom: jotai.Atom; + termTransparencyAtom: jotai.Atom; noPadding: jotai.PrimitiveAtom; endIconButtons: jotai.Atom; shellProcFullStatus: jotai.PrimitiveAtom; @@ -203,10 +205,17 @@ class TermViewModel implements ViewModel { return get(getOverrideConfigAtom(this.blockId, "term:theme")) ?? DefaultTermTheme; }); }); + this.termTransparencyAtom = useBlockAtom(blockId, "termtransparencyatom", () => { + return jotai.atom((get) => { + let value = get(getOverrideConfigAtom(this.blockId, "term:transparency")) ?? 0.5; + return boundNumber(value, 0, 1); + }); + }); this.blockBg = jotai.atom((get) => { const fullConfig = get(atoms.fullConfigAtom); const themeName = get(this.termThemeNameAtom); - const [_, bgcolor] = computeTheme(fullConfig, themeName); + const termTransparency = get(this.termTransparencyAtom); + const [_, bgcolor] = computeTheme(fullConfig, themeName, termTransparency); if (bgcolor != null) { return { bg: bgcolor }; } @@ -407,6 +416,11 @@ class TermViewModel implements ViewModel { event.preventDefault(); event.stopPropagation(); return false; + } else if (keyutil.checkKeyPressed(waveEvent, "Cmd:k")) { + event.preventDefault(); + event.stopPropagation(); + this.termRef.current?.terminal?.clear(); + return false; } const shellProcStatus = globalStore.get(this.shellProcStatus); if ((shellProcStatus == "done" || shellProcStatus == "init") && keyutil.checkKeyPressed(waveEvent, "Enter")) { @@ -453,6 +467,7 @@ class TermViewModel implements ViewModel { const termThemeKeys = Object.keys(termThemes); const curThemeName = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:theme")); const defaultFontSize = globalStore.get(getSettingsKeyAtom("term:fontsize")) ?? 12; + const transparencyMeta = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:transparency")); const blockData = globalStore.get(this.blockAtom); const overrideFontSize = blockData?.meta?.["term:fontsize"]; @@ -474,6 +489,41 @@ class TermViewModel implements ViewModel { checked: curThemeName == null, click: () => this.setTerminalTheme(null), }); + const transparencySubMenu: ContextMenuItem[] = []; + transparencySubMenu.push({ + label: "Default", + type: "checkbox", + checked: transparencyMeta == null, + click: () => { + RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { "term:transparency": null }, + }); + }, + }); + transparencySubMenu.push({ + label: "Transparent Background", + type: "checkbox", + checked: transparencyMeta == 0.5, + click: () => { + RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { "term:transparency": 0.5 }, + }); + }, + }); + transparencySubMenu.push({ + label: "No Transparency", + type: "checkbox", + checked: transparencyMeta == 0, + click: () => { + RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { "term:transparency": 0 }, + }); + }, + }); + const fontSizeSubMenu: ContextMenuItem[] = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].map( (fontSize: number) => { return { @@ -508,6 +558,10 @@ class TermViewModel implements ViewModel { label: "Font Size", submenu: fontSizeSubMenu, }); + fullMenu.push({ + label: "Transparency", + submenu: transparencySubMenu, + }); fullMenu.push({ type: "separator" }); fullMenu.push({ label: "Force Restart Controller", @@ -734,7 +788,8 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { React.useEffect(() => { const fullConfig = globalStore.get(atoms.fullConfigAtom); const termThemeName = globalStore.get(model.termThemeNameAtom); - const [termTheme, _] = computeTheme(fullConfig, termThemeName); + const termTransparency = globalStore.get(model.termTransparencyAtom); + const [termTheme, _] = computeTheme(fullConfig, termThemeName, termTransparency); let termScrollback = 1000; if (termSettings?.["term:scrollback"]) { termScrollback = Math.floor(termSettings["term:scrollback"]); diff --git a/frontend/app/view/term/termtheme.ts b/frontend/app/view/term/termtheme.ts index 8852ae15a..32937b5be 100644 --- a/frontend/app/view/term/termtheme.ts +++ b/frontend/app/view/term/termtheme.ts @@ -17,7 +17,8 @@ interface TermThemeProps { const TermThemeUpdater = ({ blockId, model, termRef }: TermThemeProps) => { const fullConfig = useAtomValue(atoms.fullConfigAtom); const blockTermTheme = useAtomValue(model.termThemeNameAtom); - const [theme, _] = computeTheme(fullConfig, blockTermTheme); + const transparency = useAtomValue(model.termTransparencyAtom); + const [theme, _] = computeTheme(fullConfig, blockTermTheme, transparency); useEffect(() => { if (termRef.current?.terminal) { termRef.current.terminal.options.theme = theme; diff --git a/frontend/app/view/term/termutil.ts b/frontend/app/view/term/termutil.ts index 1bed0e6d5..6b2eb357c 100644 --- a/frontend/app/view/term/termutil.ts +++ b/frontend/app/view/term/termutil.ts @@ -2,14 +2,32 @@ // SPDX-License-Identifier: Apache-2.0 export const DefaultTermTheme = "default-dark"; +import { colord } from "colord"; -// returns (theme, bgcolor) -function computeTheme(fullConfig: FullConfigType, themeName: string): [TermThemeType, string] { +function applyTransparencyToColor(hexColor: string, transparency: number): string { + const alpha = 1 - transparency; // transparency is already 0-1 + return colord(hexColor).alpha(alpha).toHex(); +} + +// returns (theme, bgcolor, transparency (0 - 1.0)) +function computeTheme( + fullConfig: FullConfigType, + themeName: string, + termTransparency: number +): [TermThemeType, string] { let theme: TermThemeType = fullConfig?.termthemes?.[themeName]; if (theme == null) { theme = fullConfig?.termthemes?.[DefaultTermTheme] || ({} as any); } const themeCopy = { ...theme }; + if (termTransparency != null && termTransparency > 0) { + if (themeCopy.background) { + themeCopy.background = applyTransparencyToColor(themeCopy.background, termTransparency); + } + if (themeCopy.selectionBackground) { + themeCopy.selectionBackground = applyTransparencyToColor(themeCopy.selectionBackground, termTransparency); + } + } let bgcolor = themeCopy.background; themeCopy.background = "#00000000"; return [themeCopy, bgcolor]; diff --git a/frontend/app/view/waveai/waveai.scss b/frontend/app/view/waveai/waveai.scss index 4a2020e5c..2d463fd88 100644 --- a/frontend/app/view/waveai/waveai.scss +++ b/frontend/app/view/waveai/waveai.scss @@ -4,100 +4,99 @@ .waveai { display: flex; flex-direction: column; - overflow: hidden; height: 100%; width: 100%; .waveai-chat { - flex-grow: 1; - > .scrollable { - flex-flow: column nowrap; - margin-bottom: 0; + flex: 1 1 auto; + overflow: hidden; + .chat-window-container { overflow-y: auto; - min-height: 100%; + margin-bottom: 0; + height: 100%; .chat-window { + flex-flow: column nowrap; display: flex; - flex-direction: column; gap: 8px; // This is the filler that will push the chat messages to the bottom until the chat window is full .filler { flex: 1 1 auto; } - } - .chat-msg-container { - display: flex; - gap: 8px; - .chat-msg { - margin: 10px 0; + .chat-msg-container { display: flex; - align-items: flex-start; - border-radius: 8px; - - &.chat-msg-header { + gap: 8px; + .chat-msg { + margin: 10px 0; display: flex; - flex-direction: column; - justify-content: flex-start; + align-items: flex-start; + border-radius: 8px; - .icon-box { - padding-top: 0; - border-radius: 4px; - background-color: rgb(from var(--highlight-bg-color) r g b / 0.05); + &.chat-msg-header { display: flex; - padding: 6px; + flex-direction: column; + justify-content: flex-start; + + .icon-box { + padding-top: 0; + border-radius: 4px; + background-color: rgb(from var(--highlight-bg-color) r g b / 0.05); + display: flex; + padding: 6px; + } } - } - &.chat-msg-assistant { - color: var(--main-text-color); - background-color: rgb(from var(--highlight-bg-color) r g b / 0.1); - margin-right: auto; - padding: 10px; - max-width: 85%; - - .markdown { - width: 100%; - - pre { - white-space: pre-wrap; - word-break: break-word; - max-width: 100%; - overflow-x: auto; - margin-left: 0; + &.chat-msg-assistant { + color: var(--main-text-color); + background-color: rgb(from var(--highlight-bg-color) r g b / 0.1); + margin-right: auto; + padding: 10px; + max-width: 85%; + + .markdown { + width: 100%; + + pre { + white-space: pre-wrap; + word-break: break-word; + max-width: 100%; + overflow-x: auto; + margin-left: 0; + } } } - } - &.chat-msg-user { - margin-left: auto; - padding: 10px; - max-width: 85%; - background-color: rgb(from var(--accent-color) r g b / 0.15); - } + &.chat-msg-user { + margin-left: auto; + padding: 10px; + max-width: 85%; + background-color: rgb(from var(--accent-color) r g b / 0.15); + } - &.chat-msg-error { - color: var(--main-text-color); - background-color: rgb(from var(--error-color) r g b / 0.25); - margin-right: auto; - padding: 10px; - max-width: 85%; - - .markdown { - width: 100%; - - pre { - white-space: pre-wrap; - word-break: break-word; - max-width: 100%; - overflow-x: auto; - margin-left: 0; + &.chat-msg-error { + color: var(--main-text-color); + background-color: rgb(from var(--error-color) r g b / 0.25); + margin-right: auto; + padding: 10px; + max-width: 85%; + + .markdown { + width: 100%; + + pre { + white-space: pre-wrap; + word-break: break-word; + max-width: 100%; + overflow-x: auto; + margin-left: 0; + } } } - } - &.typing-indicator { - margin-top: 4px; + &.typing-indicator { + margin-top: 4px; + } } } } @@ -105,6 +104,7 @@ } .waveai-controls { + flex: 0 0 auto; display: flex; flex-direction: row; align-items: center; diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index 0bdc2412b..6ac27e721 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -13,9 +13,11 @@ import { BlockService, ObjectService } from "@/store/services"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; import { fireAndForget, isBlank, makeIconClass } from "@/util/util"; import { atom, Atom, PrimitiveAtom, useAtomValue, WritableAtom } from "jotai"; +import { splitAtom } from "jotai/utils"; import type { OverlayScrollbars } from "overlayscrollbars"; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; +import { debounce, throttle } from "throttle-debounce"; import "./waveai.scss"; interface ChatMessageType { @@ -29,7 +31,7 @@ const outline = "2px solid var(--accent-color)"; const slidingWindowSize = 30; interface ChatItemProps { - chatItem: ChatMessageType; + chatItemAtom: Atom; model: WaveAiModel; } @@ -72,6 +74,8 @@ export class WaveAiModel implements ViewModel { preIconButton?: Atom; endIconButtons?: Atom; messagesAtom: PrimitiveAtom>; + messagesSplitAtom: SplitAtom>; + latestMessageAtom: Atom; addMessageAtom: WritableAtom; updateLastMessageAtom: WritableAtom; removeLastMessageAtom: WritableAtom; @@ -92,6 +96,8 @@ export class WaveAiModel implements ViewModel { this.viewIcon = atom("sparkles"); this.viewName = atom("Wave AI"); this.messagesAtom = atom([]); + this.messagesSplitAtom = splitAtom(this.messagesAtom); + this.latestMessageAtom = atom((get) => get(this.messagesAtom).slice(-1)[0]); this.presetKey = atom((get) => { const metaPresetKey = get(this.blockAtom).meta["ai:preset"]; const globalPresetKey = get(atoms.settingsAtom)["ai:preset"]; @@ -405,10 +411,8 @@ export class WaveAiModel implements ViewModel { } useWaveAi() { - const messages = useAtomValue(this.messagesAtom); return { - messages, - sendMessage: this.sendMessage.bind(this), + sendMessage: this.sendMessage.bind(this) as (text: string) => void, }; } @@ -431,10 +435,9 @@ function makeWaveAiViewModel(blockId: string): WaveAiModel { return waveAiModel; } -const ChatItem = ({ chatItem, model }: ChatItemProps) => { +const ChatItem = ({ chatItemAtom, model }: ChatItemProps) => { + const chatItem = useAtomValue(chatItemAtom); const { user, text } = chatItem; - const cssVar = "--panel-bg-color"; - const panelBgColor = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim(); const fontSize = useOverrideConfigAtom(model.blockId, "ai:fontsize"); const fixedFontSize = useOverrideConfigAtom(model.blockId, "ai:fixedfontsize"); const renderContent = useMemo(() => { @@ -503,44 +506,66 @@ const ChatItem = ({ chatItem, model }: ChatItemProps) => { interface ChatWindowProps { chatWindowRef: React.RefObject; - messages: ChatMessageType[]; msgWidths: Object; model: WaveAiModel; } const ChatWindow = memo( - forwardRef(({ chatWindowRef, messages, msgWidths, model }, ref) => { - const [isUserScrolling, setIsUserScrolling] = useState(false); - + forwardRef(({ chatWindowRef, msgWidths, model }, ref) => { + const isUserScrolling = useRef(false); const osRef = useRef(null); - const prevMessagesLenRef = useRef(messages.length); + const splitMessages = useAtomValue(model.messagesSplitAtom) as Atom[]; + const latestMessage = useAtomValue(model.latestMessageAtom); + const prevMessagesLenRef = useRef(splitMessages.length); useImperativeHandle(ref, () => osRef.current as OverlayScrollbarsComponentRef); + const handleNewMessage = useCallback( + throttle(100, (messagesLen: number) => { + if (osRef.current?.osInstance()) { + console.log("handleNewMessage", messagesLen, isUserScrolling.current); + const { viewport } = osRef.current.osInstance().elements(); + if (prevMessagesLenRef.current !== messagesLen || !isUserScrolling.current) { + viewport.scrollTo({ + behavior: "auto", + top: chatWindowRef.current?.scrollHeight || 0, + }); + } + + prevMessagesLenRef.current = messagesLen; + } + }), + [] + ); + useEffect(() => { - if (osRef.current && osRef.current.osInstance()) { + handleNewMessage(splitMessages.length); + }, [splitMessages, latestMessage]); + + // Wait 300 ms after the user stops scrolling to determine if the user is within 300px of the bottom of the chat window. + // If so, unset the user scrolling flag. + const determineUnsetScroll = useCallback( + debounce(300, () => { const { viewport } = osRef.current.osInstance().elements(); - const curMessagesLen = messages.length; - if (prevMessagesLenRef.current !== curMessagesLen || !isUserScrolling) { - setIsUserScrolling(false); - viewport.scrollTo({ - behavior: "auto", - top: chatWindowRef.current?.scrollHeight || 0, - }); + if (viewport.scrollTop > chatWindowRef.current?.clientHeight - viewport.clientHeight - 100) { + isUserScrolling.current = false; } + }), + [] + ); - prevMessagesLenRef.current = curMessagesLen; - } - }, [messages, isUserScrolling]); + const handleUserScroll = useCallback( + throttle(100, () => { + isUserScrolling.current = true; + determineUnsetScroll(); + }), + [] + ); useEffect(() => { - if (osRef.current && osRef.current.osInstance()) { + if (osRef.current?.osInstance()) { const { viewport } = osRef.current.osInstance().elements(); - const handleUserScroll = () => { - setIsUserScrolling(true); - }; - viewport.addEventListener("wheel", handleUserScroll, { passive: true }); viewport.addEventListener("touchmove", handleUserScroll, { passive: true }); @@ -571,14 +596,14 @@ const ChatWindow = memo( return (
- {messages.map((chitem, idx) => ( - + {splitMessages.map((chitem, idx) => ( + ))}
@@ -652,7 +677,7 @@ const ChatInput = forwardRef( ); const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => { - const { messages, sendMessage } = model.useWaveAi(); + const { sendMessage } = model.useWaveAi(); const waveaiRef = useRef(null); const chatWindowRef = useRef(null); const osRef = useRef(null); @@ -716,7 +741,7 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => { sendMessage(value); setValue(""); setSelectedBlockIdx(null); - }, [messages, value]); + }, [value]); const updateScrollTop = () => { const pres = chatWindowRef.current?.querySelectorAll("pre"); @@ -823,13 +848,7 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => { return (
- +
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index b76f911bd..f2c14723a 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -491,6 +491,7 @@ declare global { "term:scrollback"?: number; "term:vdomblockid"?: string; "term:vdomtoolbarblockid"?: string; + "term:transparency"?: number; "web:zoom"?: number; "markdown:fontsize"?: number; "markdown:fixedfontsize"?: number; @@ -641,6 +642,7 @@ declare global { "term:localshellopts"?: string[]; "term:scrollback"?: number; "term:copyonselect"?: boolean; + "term:transparency"?: number; "editor:minimapenabled"?: boolean; "editor:stickyscrollenabled"?: boolean; "editor:wordwrap"?: boolean; @@ -678,6 +680,8 @@ declare global { "window:magnifiedblocksize"?: number; "window:magnifiedblockblurprimarypx"?: number; "window:magnifiedblockblursecondarypx"?: number; + "window:confirmclose"?: boolean; + "window:savelastwindow"?: boolean; "telemetry:*"?: boolean; "telemetry:enabled"?: boolean; "conn:*"?: boolean; diff --git a/go.mod b/go.mod index 772c34c62..448f0f818 100644 --- a/go.mod +++ b/go.mod @@ -45,9 +45,9 @@ require ( github.com/ubuntu/decorate v0.0.0-20230125165522-2d5b0a9bb117 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/atomic v1.7.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/net v0.33.0 // indirect ) -replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20241027232332-ed124367682d +replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34 replace github.com/creack/pty => github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b diff --git a/go.sum b/go.sum index 8429144ec..d560a54ab 100644 --- a/go.sum +++ b/go.sum @@ -90,16 +90,16 @@ github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b h1:wFBKF5k5xbJQU8bYgc github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b/go.mod h1:N1CYNinssZru+ikvYTgVbVeSi21thHUTCoJ9xMvWe+s= github.com/wavetermdev/htmltoken v0.2.0 h1:sFVPPemlDv7/jg7n4Hx1AEF2m9MVAFjFpELWfhi/DlM= github.com/wavetermdev/htmltoken v0.2.0/go.mod h1:5FM0XV6zNYiNza2iaTcFGj+hnMtgqumFHO31Z8euquk= -github.com/wavetermdev/ssh_config v0.0.0-20241027232332-ed124367682d h1:ArHaUBaiQWUqBzM2G/oLlm3Be0kwUMDt9vTNOWIfOd0= -github.com/wavetermdev/ssh_config v0.0.0-20241027232332-ed124367682d/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34 h1:I8VZVTZEXhnzfN7jB9a7TZYpzNO48sCUWMRXHM9XWSA= +github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/package.json b/package.json index 6497727ea..eadcdb21d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "productName": "Wave", "description": "Open-Source AI-Native Terminal Built for Seamless Workflows", "license": "Apache-2.0", - "version": "0.10.2-beta.0", + "version": "0.10.4", "homepage": "https://waveterm.dev", "build": { "appId": "dev.commandline.waveterm" diff --git a/pkg/filestore/blockstore_cache.go b/pkg/filestore/blockstore_cache.go index f8608654b..ab48e7a83 100644 --- a/pkg/filestore/blockstore_cache.go +++ b/pkg/filestore/blockstore_cache.go @@ -227,6 +227,9 @@ func (entry *CacheEntry) readAt(ctx context.Context, offset int64, size int64, r offset += truncateAmt size -= truncateAmt } + if size <= 0 { + return realDataOffset, nil, nil + } } partMap := file.computePartMap(offset, size) dataEntryMap, err := entry.loadDataPartsForRead(ctx, getPartIdxsFromMap(partMap)) diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index bcc4291c2..3b2b0cc90 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -23,6 +23,7 @@ import ( "github.com/kevinburke/ssh_config" "github.com/skeema/knownhosts" + "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/trimquotes" "github.com/wavetermdev/waveterm/pkg/userinput" "github.com/wavetermdev/waveterm/pkg/util/shellutil" @@ -750,7 +751,13 @@ func combineSshKeywords(userProvidedOpts *wshrpc.ConnKeywords, configKeywords *w // note that a `var == "yes"` will default to false // but `var != "no"` will default to true // when given unexpected strings -func findSshConfigKeywords(hostPattern string) (*wshrpc.ConnKeywords, error) { +func findSshConfigKeywords(hostPattern string) (connKeywords *wshrpc.ConnKeywords, outErr error) { + defer func() { + err := panichandler.PanicHandler("sshclient:find-ssh-config-keywords") + if err != nil { + outErr = err + } + }() WaveSshConfigUserSettings().ReloadConfigs() sshKeywords := &wshrpc.ConnKeywords{} var err error diff --git a/pkg/service/workspaceservice/workspaceservice.go b/pkg/service/workspaceservice/workspaceservice.go index 1c86ff77b..b6c97ac8b 100644 --- a/pkg/service/workspaceservice/workspaceservice.go +++ b/pkg/service/workspaceservice/workspaceservice.go @@ -45,10 +45,13 @@ func (svc *WorkspaceService) UpdateWorkspace_Meta() tsgenmeta.MethodMeta { func (svc *WorkspaceService) UpdateWorkspace(ctx context.Context, workspaceId string, name string, icon string, color string, applyDefaults bool) (waveobj.UpdatesRtnType, error) { ctx = waveobj.ContextWithUpdates(ctx) - _, err := wcore.UpdateWorkspace(ctx, workspaceId, name, icon, color, applyDefaults) + _, updated, err := wcore.UpdateWorkspace(ctx, workspaceId, name, icon, color, applyDefaults) if err != nil { return nil, fmt.Errorf("error updating workspace: %w", err) } + if !updated { + return nil, nil + } wps.Broker.Publish(wps.WaveEvent{ Event: wps.Event_WorkspaceUpdate}) diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index 2c811d85c..a6fcf00e4 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -215,7 +215,7 @@ func StartWslShellProc(ctx context.Context, termSize waveobj.TermSize, cmdStr st } if isZshShell(shellPath) { - shellOpts = append(shellOpts, fmt.Sprintf(`ZDOTDIR="%s/.waveterm/%s"`, homeDir, shellutil.ZshIntegrationDir)) + shellOpts = append(shellOpts, fmt.Sprintf(`ZDOTDIR=%s/.waveterm/%s`, homeDir, shellutil.ZshIntegrationDir)) } shellOpts = append(shellOpts, shellPath) shellOpts = append(shellOpts, subShellOpts...) diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index 9071bf326..6b65249b6 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -93,6 +93,7 @@ const ( MetaKey_TermScrollback = "term:scrollback" MetaKey_TermVDomSubBlockId = "term:vdomblockid" MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid" + MetaKey_TermTransparency = "term:transparency" MetaKey_WebZoom = "web:zoom" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index 41f0b7be5..bbf30cffb 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -94,6 +94,7 @@ type MetaTSType struct { TermScrollback *int `json:"term:scrollback,omitempty"` TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"` TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"` + TermTransparency *float64 `json:"term:transparency,omitempty"` // default 0.5 WebZoom float64 `json:"web:zoom,omitempty"` diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json index fa8d98f88..3da7e6fd2 100644 --- a/pkg/wconfig/defaultconfig/settings.json +++ b/pkg/wconfig/defaultconfig/settings.json @@ -7,7 +7,7 @@ "autoupdate:installonquit": true, "autoupdate:intervalms": 3600000, "conn:askbeforewshinstall": true, - "conn:wshenabled": true, + "conn:wshenabled": true, "editor:minimapenabled": true, "web:defaulturl": "https://github.com/wavetermdev/waveterm", "web:defaultsearch": "https://www.google.com/search?q={query}", @@ -18,6 +18,8 @@ "window:magnifiedblocksize": 0.9, "window:magnifiedblockblurprimarypx": 10, "window:magnifiedblockblursecondarypx": 2, + "window:confirmclose": true, + "window:savelastwindow": true, "telemetry:enabled": true, "term:copyonselect": true } diff --git a/pkg/wconfig/defaultconfig/termthemes.json b/pkg/wconfig/defaultconfig/termthemes.json index ea8a5f1a0..d0a667f0a 100644 --- a/pkg/wconfig/defaultconfig/termthemes.json +++ b/pkg/wconfig/defaultconfig/termthemes.json @@ -22,12 +22,37 @@ "cmdtext": "#f0f0f0", "foreground": "#c1c1c1", "selectionBackground": "", - "background": "#00000077", + "background": "#000000", "cursor": "" }, + "onedarkpro": { + "display:name": "One Dark Pro", + "display:order": 2, + "background": "#21252B", + "foreground": "#ABB2BF", + "cursor": "#D7DAE0", + "black": "#3F4451", + "red": "#E06C75", + "green": "#98C379", + "yellow": "#D18F52", + "blue": "#61AFEF", + "magenta": "#C678DD", + "cyan": "#42B3C2", + "white": "#D7DAE0", + "brightBlack": "#4F5666", + "brightRed": "#FF616E", + "brightGreen": "#A5E075", + "brightYellow": "#F0A45D", + "brightBlue": "#4DC4FF", + "brightMagenta": "#DE73FF", + "brightCyan": "#4CD1E0", + "brightWhite": "#E6E6E6", + "gray": "#495162", + "cmdtext": "#ABB2BF" + }, "dracula": { "display:name": "Dracula", - "display:order": 2, + "display:order": 3, "black": "#21222C", "red": "#FF5555", "green": "#50FA7B", @@ -47,13 +72,12 @@ "gray": "#6272A4", "cmdtext": "#F8F8F2", "foreground": "#F8F8F2", - "selectionBackground": "#44475a", "background": "#282a36", "cursor": "#f8f8f2" }, "monokai": { "display:name": "Monokai", - "display:order": 3, + "display:order": 4, "black": "#1B1D1E", "red": "#F92672", "green": "#A6E22E", @@ -73,13 +97,12 @@ "gray": "#75715E", "cmdtext": "#F8F8F2", "foreground": "#F8F8F2", - "selectionBackground": "#49483E", "background": "#272822", "cursor": "#F8F8F2" }, "campbell": { "display:name": "Campbell", - "display:order": 4, + "display:order": 5, "black": "#0C0C0C", "red": "#C50F1F", "green": "#13A10E", @@ -99,13 +122,13 @@ "gray": "#767676", "cmdtext": "#CCCCCC", "foreground": "#CCCCCC", - "selectionBackground": "#3A96DD", + "selectionBackground": "#3A96DD77", "background": "#0C0C0C", "cursor": "#CCCCCC" }, "warmyellow": { "display:name": "Warm Yellow", - "display:order": 4, + "display:order": 6, "black": "#3C3228", "red": "#E67E22", "green": "#A5D6A7", @@ -124,33 +147,32 @@ "brightWhite": "#FFFFFF", "background": "#2B2620", "foreground": "#F2E6D4", - "selectionBackground": "#B7950B", + "selectionBackground": "#B7950B77", "cursor": "#F9D784" }, - "onedarkpro": { - "display:name": "One Dark Pro", - "display:order": 1.5, - "background": "#282C34", - "foreground": "#ABB2BF", - "cursor": "#D7DAE0", - "selectionBackground": "#528BFF", - "black": "#3F4451", - "red": "#E05561", - "green": "#8CC265", - "yellow": "#D18F52", - "blue": "#4AA5F0", - "magenta": "#C162DE", - "cyan": "#42B3C2", - "white": "#D7DAE0", - "brightBlack": "#4F5666", - "brightRed": "#FF616E", - "brightGreen": "#A5E075", - "brightYellow": "#F0A45D", - "brightBlue": "#4DC4FF", - "brightMagenta": "#DE73FF", - "brightCyan": "#4CD1E0", - "brightWhite": "#E6E6E6", - "gray": "#495162", - "cmdtext": "#ABB2BF" + "rosepine": { + "display:name": "Rose Pine", + "display:order": 7, + "black": "#26233a", + "red": "#eb6f92", + "green": "#3e8fb0", + "yellow": "#f6c177", + "blue": "#9ccfd8", + "magenta": "#c4a7e7", + "cyan": "#ebbcba", + "white": "#e0def4", + "brightBlack": "#908caa", + "brightRed": "#ff8cab", + "brightGreen": "#9ccfb0", + "brightYellow": "#ffd196", + "brightBlue": "#bee6e0", + "brightMagenta": "#e2c4ff", + "brightCyan": "#ffd1d0", + "brightWhite": "#fffaf3", + "gray": "#908caa", + "cmdtext": "#e0def4", + "foreground": "#e0def4", + "background": "#191724", + "cursor": "#524f67" } } diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index e0c888ec6..ae01a5e81 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -33,6 +33,7 @@ const ( ConfigKey_TermLocalShellOpts = "term:localshellopts" ConfigKey_TermScrollback = "term:scrollback" ConfigKey_TermCopyOnSelect = "term:copyonselect" + ConfigKey_TermTransparency = "term:transparency" ConfigKey_EditorMinimapEnabled = "editor:minimapenabled" ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled" @@ -79,6 +80,8 @@ const ( ConfigKey_WindowMagnifiedBlockSize = "window:magnifiedblocksize" ConfigKey_WindowMagnifiedBlockBlurPrimaryPx = "window:magnifiedblockblurprimarypx" ConfigKey_WindowMagnifiedBlockBlurSecondaryPx = "window:magnifiedblockblursecondarypx" + ConfigKey_WindowConfirmClose = "window:confirmclose" + ConfigKey_WindowSaveLastWindow = "window:savelastwindow" ConfigKey_TelemetryClear = "telemetry:*" ConfigKey_TelemetryEnabled = "telemetry:enabled" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 437375429..685ac7f83 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -60,6 +60,7 @@ type SettingsType struct { TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` TermScrollback *int64 `json:"term:scrollback,omitempty"` TermCopyOnSelect *bool `json:"term:copyonselect,omitempty"` + TermTransparency *float64 `json:"term:transparency,omitempty"` EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"` EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"` @@ -106,6 +107,8 @@ type SettingsType struct { WindowMagnifiedBlockSize *float64 `json:"window:magnifiedblocksize,omitempty"` WindowMagnifiedBlockBlurPrimaryPx *int64 `json:"window:magnifiedblockblurprimarypx,omitempty"` WindowMagnifiedBlockBlurSecondaryPx *int64 `json:"window:magnifiedblockblursecondarypx,omitempty"` + WindowConfirmClose bool `json:"window:confirmclose,omitempty"` + WindowSaveLastWindow bool `json:"window:savelastwindow,omitempty"` TelemetryClear bool `json:"telemetry:*,omitempty"` TelemetryEnabled bool `json:"telemetry:enabled,omitempty"` diff --git a/pkg/wcore/wcore.go b/pkg/wcore/wcore.go index 780344135..4b7b1558a 100644 --- a/pkg/wcore/wcore.go +++ b/pkg/wcore/wcore.go @@ -25,6 +25,7 @@ func EnsureInitialData() error { ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() client, err := wstore.DBGetSingleton[*waveobj.Client](ctx) + firstLaunch := false if err == wstore.ErrNotFound { client, err = CreateClient(ctx) if err != nil { @@ -34,6 +35,7 @@ func EnsureInitialData() error { if migrateErr != nil { log.Printf("error migrating old history: %v\n", migrateErr) } + firstLaunch = true } if client.TempOID == "" { log.Println("client.TempOID is empty") @@ -53,12 +55,16 @@ func EnsureInitialData() error { log.Println("client has windows") return nil } - log.Println("client has no windows, creating starter workspace") - starterWs, err := CreateWorkspace(ctx, "Starter workspace", "custom@wave-logo-solid", "#58C142", false, true) - if err != nil { - return fmt.Errorf("error creating starter workspace: %w", err) + wsId := "" + if firstLaunch { + log.Println("client has no windows and first launch, creating starter workspace") + starterWs, err := CreateWorkspace(ctx, "Starter workspace", "custom@wave-logo-solid", "#58C142", false, true) + if err != nil { + return fmt.Errorf("error creating starter workspace: %w", err) + } + wsId = starterWs.OID } - _, err = CreateWindow(ctx, nil, starterWs.OID) + _, err = CreateWindow(ctx, nil, wsId) if err != nil { return fmt.Errorf("error creating window: %w", err) } diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index b3143b2ab..1cc8ce1cf 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -68,26 +68,34 @@ func CreateWorkspace(ctx context.Context, name string, icon string, color string wps.Broker.Publish(wps.WaveEvent{ Event: wps.Event_WorkspaceUpdate}) - return UpdateWorkspace(ctx, ws.OID, name, icon, color, applyDefaults) + ws, _, err = UpdateWorkspace(ctx, ws.OID, name, icon, color, applyDefaults) + return ws, err } -func UpdateWorkspace(ctx context.Context, workspaceId string, name string, icon string, color string, applyDefaults bool) (*waveobj.Workspace, error) { +// Returns updated workspace, whether it was updated, error. +func UpdateWorkspace(ctx context.Context, workspaceId string, name string, icon string, color string, applyDefaults bool) (*waveobj.Workspace, bool, error) { ws, err := GetWorkspace(ctx, workspaceId) + updated := false if err != nil { - return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err) + return nil, updated, fmt.Errorf("workspace %s not found: %w", workspaceId, err) } if name != "" { ws.Name = name + updated = true } else if applyDefaults && ws.Name == "" { ws.Name = fmt.Sprintf("New Workspace (%s)", ws.OID[0:5]) + updated = true } if icon != "" { ws.Icon = icon + updated = true } else if applyDefaults && ws.Icon == "" { ws.Icon = WorkspaceIcons[0] + updated = true } if color != "" { ws.Color = color + updated = true } else if applyDefaults && ws.Color == "" { wsList, err := ListWorkspaces(ctx) if err != nil { @@ -95,9 +103,12 @@ func UpdateWorkspace(ctx context.Context, workspaceId string, name string, icon wsList = waveobj.WorkspaceList{} } ws.Color = WorkspaceColors[len(wsList)%len(WorkspaceColors)] + updated = true } - wstore.DBUpdate(ctx, ws) - return ws, nil + if updated { + wstore.DBUpdate(ctx, ws) + } + return ws, updated, nil } // If force is true, it will delete even if workspace is named. diff --git a/pkg/wshutil/wshrpc.go b/pkg/wshutil/wshrpc.go index 4bbbbd01e..24bb8d483 100644 --- a/pkg/wshutil/wshrpc.go +++ b/pkg/wshutil/wshrpc.go @@ -342,7 +342,10 @@ func (w *WshRpc) runServer() { continue } if msg.IsRpcRequest() { - go w.handleRequest(&msg) + go func() { + defer panichandler.PanicHandler("handleRequest:goroutine") + w.handleRequest(&msg) + }() } else { respCh := w.getResponseCh(msg.ResId) if respCh == nil {