From 101d09b84200f5cc412486818e222c90cb5007b9 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Wed, 25 Mar 2026 13:06:57 -0700 Subject: [PATCH 01/11] fix for terrible background color for default "ls" output on pwsh (#3119) --- pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh b/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh index cc002b57a9..6fd70eee14 100644 --- a/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh +++ b/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh @@ -16,6 +16,10 @@ if ($PSVersionTable.PSVersion.Major -lt 7) { return # skip OSC setup entirely } +if ($PSStyle.FileInfo.Directory -eq "`e[44;1m") { + $PSStyle.FileInfo.Directory = "`e[34;1m" +} + $Global:_WAVETERM_SI_FIRSTPROMPT = $true # shell integration From 0b29c49076e2dcc7a7f34306d2a297d3ebb47d2c Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Thu, 26 Mar 2026 04:31:01 +0800 Subject: [PATCH 02/11] fix: use fspath.Base to strip Windows paths on SSH remote drag-drop (#3118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #3079 ## Summary When dragging a local file from Windows to an SSH remote connection, the full Windows path (e.g. `D:\package\AA.tar`) was being passed to `filepath.Base` on the remote (Linux) side. Since `filepath.Base` on Linux does not recognize backslashes as separators, the full path was used as the destination filename. - In `RemoteFileCopyCommand` (`wshremote_file.go:159`), replace `filepath.Base(srcConn.Path)` with `fspath.Base(srcConn.Path)` - `fspath.Base` calls `ToSlash` before `path.Base`, converting backslashes to forward slashes first, so `D:\package\AA.tar` correctly yields `AA.tar` on any OS - Same-host copies at line 86 use `filepath.Base(srcPathCleaned)` and are unaffected — those run on the same OS where `filepath.Base` is correct ## Test Plan - Added `pkg/remote/fileshare/fspath/fspath_test.go` with table-driven tests for `fspath.Base`: - Windows path with backslashes: `D:\package\AA.tar` → `AA.tar` - Windows path with forward slashes: `D:/package/AA.tar` → `AA.tar` - Unix path: `/home/user/file.txt` → `file.txt` - Filename only: `file.txt` → `file.txt` - `go test ./pkg/remote/fileshare/fspath/...` passes Signed-off-by: majiayu000 <1835304752@qq.com> --- pkg/remote/fileshare/fspath/fspath_test.go | 21 +++++++++++++++++++++ pkg/wshrpc/wshremote/wshremote_file.go | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 pkg/remote/fileshare/fspath/fspath_test.go diff --git a/pkg/remote/fileshare/fspath/fspath_test.go b/pkg/remote/fileshare/fspath/fspath_test.go new file mode 100644 index 0000000000..c634f665ce --- /dev/null +++ b/pkg/remote/fileshare/fspath/fspath_test.go @@ -0,0 +1,21 @@ +package fspath + +import "testing" + +func TestBase(t *testing.T) { + tests := []struct { + path string + want string + }{ + {`D:\package\AA.tar`, "AA.tar"}, + {`D:/package/AA.tar`, "AA.tar"}, + {"/home/user/file.txt", "file.txt"}, + {"file.txt", "file.txt"}, + } + for _, tt := range tests { + got := Base(tt.path) + if got != tt.want { + t.Errorf("Base(%q) = %q, want %q", tt.path, got, tt.want) + } + } +} diff --git a/pkg/wshrpc/wshremote/wshremote_file.go b/pkg/wshrpc/wshremote/wshremote_file.go index 845fb64ac4..3589cc998c 100644 --- a/pkg/wshrpc/wshremote/wshremote_file.go +++ b/pkg/wshrpc/wshremote/wshremote_file.go @@ -18,6 +18,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/panichandler" "github.com/wavetermdev/waveterm/pkg/remote/connparse" + "github.com/wavetermdev/waveterm/pkg/remote/fileshare/fspath" "github.com/wavetermdev/waveterm/pkg/remote/fileshare/wshfs" "github.com/wavetermdev/waveterm/pkg/util/fileutil" "github.com/wavetermdev/waveterm/pkg/util/utilfn" @@ -156,7 +157,7 @@ func (impl *ServerImpl) RemoteFileCopyCommand(ctx context.Context, data wshrpc.C return false, fmt.Errorf("file %q size %d exceeds transfer limit of %d bytes", data.SrcUri, srcFileInfo.Size, RemoteFileTransferSizeLimit) } - destFilePath, err := prepareDestForCopy(destPathCleaned, filepath.Base(srcConn.Path), destHasSlash, opts.Overwrite) + destFilePath, err := prepareDestForCopy(destPathCleaned, fspath.Base(srcConn.Path), destHasSlash, opts.Overwrite) if err != nil { return false, err } From cdfa11dc4140e4606ab6a9c641cd78a3949f087f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:24:01 -0700 Subject: [PATCH 03/11] Bump picomatch from 2.3.1 to 2.3.2 (#3123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
Release notes

Sourced from picomatch's releases.

2.3.2

This is a security release fixing several security relevant issues.

What's Changed

Full Changelog: https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2

Changelog

Sourced from picomatch's changelog.

Release history

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

  • Changelogs are for humans, not machines.
  • There should be an entry for every single version.
  • The same types of changes should be grouped.
  • Versions and sections should be linkable.
  • The latest version comes first.
  • The release date of each versions is displayed.
  • Mention whether you follow Semantic Versioning.

Changelog entries are classified using the following labels (from keep-a-changelog):

  • Added for new features.
  • Changed for changes in existing functionality.
  • Deprecated for soon-to-be removed features.
  • Removed for now removed features.
  • Fixed for any bug fixes.
  • Security in case of vulnerabilities.

4.0.0 (2024-02-07)

Fixes

Changed

3.0.1

Fixes

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=picomatch&package-manager=npm_and_yarn&previous-version=2.3.1&new-version=2.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/wavetermdev/waveterm/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a528967b7..9258769417 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.14.3", + "version": "0.14.4-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.14.3", + "version": "0.14.4-beta.0", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ @@ -3880,9 +3880,9 @@ } }, "node_modules/@docusaurus/core/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -11078,9 +11078,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -18764,9 +18764,9 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -22358,9 +22358,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -24055,9 +24055,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -31342,9 +31342,9 @@ } }, "node_modules/unified-args/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -32824,9 +32824,9 @@ } }, "node_modules/webpack-dev-server/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" From 24de0c1bcd9f7706be28b62e12b194eab7b2636f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:39:05 -0700 Subject: [PATCH 04/11] Deprecate the legacy `waveai` block UI and add a preview for its replacement state (#3122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [x] Inspect the current legacy Wave AI block UI and widget config references - [x] Restyle the deprecated `waveai` block message to remove the inner card/icon and position it slightly above center - [x] Remove `defwidget@ai` from default widgets and delete the related frontend special-case filtering - [x] Update the widgets preview mock data and scenarios to reflect the removed AI default widget - [x] Re-run targeted validation, review, and security scan --- 📍 Connect Copilot coding agent with [Jira](https://gh.io/cca-jira-docs), [Azure Boards](https://gh.io/cca-azure-boards-docs) or [Linear](https://gh.io/cca-linear-docs) to delegate work to Copilot in one click without leaving your project management tool. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/app/view/waveai/waveai.scss | 149 --- frontend/app/view/waveai/waveai.tsx | 914 +----------------- frontend/app/workspace/widgets.tsx | 8 +- frontend/preview/previews/waveai.preview.tsx | 53 + frontend/preview/previews/widgets.preview.tsx | 23 +- pkg/wconfig/defaultconfig/widgets.json | 12 +- 6 files changed, 81 insertions(+), 1078 deletions(-) delete mode 100644 frontend/app/view/waveai/waveai.scss create mode 100644 frontend/preview/previews/waveai.preview.tsx diff --git a/frontend/app/view/waveai/waveai.scss b/frontend/app/view/waveai/waveai.scss deleted file mode 100644 index 2d463fd88e..0000000000 --- a/frontend/app/view/waveai/waveai.scss +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -.waveai { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - - .waveai-chat { - flex: 1 1 auto; - overflow: hidden; - .chat-window-container { - overflow-y: auto; - margin-bottom: 0; - height: 100%; - - .chat-window { - flex-flow: column nowrap; - display: flex; - 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; - display: flex; - align-items: flex-start; - border-radius: 8px; - - &.chat-msg-header { - display: flex; - 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-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; - } - } - } - - &.typing-indicator { - margin-top: 4px; - } - } - } - } - } - } - - .waveai-controls { - flex: 0 0 auto; - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - gap: 10px; - padding: 8px 6px; - - .waveai-input-wrapper { - padding: 8px 12px; - flex: 1 1 auto; - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - border-radius: 6px; - border: 1px solid rgb(from var(--highlight-bg-color) r g b / 0.42); - - .waveai-input { - color: var(--main-text-color); - background-color: inherit; - resize: none; - width: 100%; - border: transparent; - outline: none; - overflow: auto; - overflow-wrap: anywhere; - height: 21px; - } - } - - .waveai-submit-button { - border-radius: 100%; - width: 27px; - aspect-ratio: 1 /1; - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - padding: 0; - } - } -} diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index c71d012a61..baf6acf711 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -1,912 +1,40 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { BlockNodeModel } from "@/app/block/blocktypes"; import { Button } from "@/app/element/button"; -import { Markdown } from "@/app/element/markdown"; -import { TypingIndicator } from "@/app/element/typingindicator"; -import { ClientModel } from "@/app/store/client-model"; -import { globalStore } from "@/app/store/jotaiStore"; -import type { TabModel } from "@/app/store/tab-model"; -import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { makeFeBlockRouteId } from "@/app/store/wshrouter"; -import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; -import { atoms, createBlock, fetchWaveFile, getApi, WOS } from "@/store/global"; -import { BlockService, ObjectService } from "@/store/services"; -import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; -import { fireAndForget, isBlank, makeIconClass, mergeMeta } 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 { - id: string; - user: string; - text: string; - isUpdating?: boolean; -} - -const outline = "2px solid var(--accent-color)"; -const slidingWindowSize = 30; - -interface ChatItemProps { - chatItemAtom: Atom; - model: WaveAiModel; -} - -function promptToMsg(prompt: WaveAIPromptMessageType): ChatMessageType { - return { - id: crypto.randomUUID(), - user: prompt.role, - text: prompt.content, - }; -} - -class AiWshClient extends WshClient { - blockId: string; - model: WaveAiModel; - - constructor(blockId: string, model: WaveAiModel) { - super(makeFeBlockRouteId(blockId)); - this.blockId = blockId; - this.model = model; - } - - handle_aisendmessage(rh: RpcResponseHelper, data: AiMessageData) { - if (isBlank(data.message)) { - return; - } - this.model.sendMessage(data.message); - } -} +import { atom } from "jotai"; +import { useCallback } from "react"; export class WaveAiModel implements ViewModel { - viewType: string; - blockId: string; - nodeModel: BlockNodeModel; - tabModel: TabModel; - blockAtom: Atom; - presetKey: Atom; - presetMap: Atom<{ [k: string]: MetaType }>; - mergedPresets: Atom; - aiOpts: Atom; - viewIcon?: Atom; - viewName?: Atom; - viewText?: Atom; - preIconButton?: Atom; - endIconButtons?: Atom; - messagesAtom: PrimitiveAtom>; - messagesSplitAtom: SplitAtom>; - latestMessageAtom: Atom; - addMessageAtom: WritableAtom; - updateLastMessageAtom: WritableAtom; - removeLastMessageAtom: WritableAtom; - simulateAssistantResponseAtom: WritableAtom>; - textAreaRef: React.RefObject; - locked: PrimitiveAtom; - cancel: boolean; - aiWshClient: AiWshClient; - - constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { - this.blockId = blockId; - this.nodeModel = nodeModel; - this.tabModel = tabModel; - this.aiWshClient = new AiWshClient(blockId, this); - DefaultRouter.registerRoute(makeFeBlockRouteId(blockId), this.aiWshClient); - this.locked = atom(false); - this.cancel = false; - this.viewType = "waveai"; - this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); - 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"]; - return metaPresetKey ?? globalPresetKey; - }); - this.presetMap = atom((get) => { - const fullConfig = get(atoms.fullConfigAtom); - const presets = fullConfig.presets; - const settings = fullConfig.settings; - return Object.fromEntries( - Object.entries(presets) - .filter(([k]) => k.startsWith("ai@")) - .map(([k, v]) => { - const aiPresetKeys = Object.keys(v).filter((k) => k.startsWith("ai:")); - const newV = { ...v }; - newV["display:name"] = - aiPresetKeys.length == 1 && aiPresetKeys.includes("ai:*") - ? `${newV["display:name"] ?? "Default"} (${settings["ai:model"]})` - : newV["display:name"]; - return [k, newV]; - }) - ); - }); - - this.addMessageAtom = atom(null, (get, set, message: ChatMessageType) => { - const messages = get(this.messagesAtom); - set(this.messagesAtom, [...messages, message]); - }); - - this.updateLastMessageAtom = atom(null, (get, set, text: string, isUpdating: boolean) => { - const messages = get(this.messagesAtom); - const lastMessage = messages[messages.length - 1]; - if (lastMessage.user == "assistant") { - const updatedMessage = { ...lastMessage, text: lastMessage.text + text, isUpdating }; - set(this.messagesAtom, [...messages.slice(0, -1), updatedMessage]); - } - }); - this.removeLastMessageAtom = atom(null, (get, set) => { - const messages = get(this.messagesAtom); - messages.pop(); - set(this.messagesAtom, [...messages]); - }); - this.simulateAssistantResponseAtom = atom(null, async (_, set, userMessage: ChatMessageType) => { - // unused at the moment. can replace the temp() function in the future - const typingMessage: ChatMessageType = { - id: crypto.randomUUID(), - user: "assistant", - text: "", - }; - - // Add a typing indicator - set(this.addMessageAtom, typingMessage); - const parts = userMessage.text.split(" "); - let currentPart = 0; - while (currentPart < parts.length) { - const part = parts[currentPart] + " "; - set(this.updateLastMessageAtom, part, true); - currentPart++; - } - set(this.updateLastMessageAtom, "", false); - }); - - this.mergedPresets = atom((get) => { - const meta = get(this.blockAtom).meta; - let settings = get(atoms.settingsAtom); - let presetKey = get(this.presetKey); - let presets = get(atoms.fullConfigAtom).presets; - let selectedPresets = presets?.[presetKey] ?? {}; - - let mergedPresets: MetaType = {}; - mergedPresets = mergeMeta(settings, selectedPresets, "ai"); - mergedPresets = mergeMeta(mergedPresets, meta, "ai"); - - return mergedPresets; - }); - - this.aiOpts = atom((get) => { - const mergedPresets = get(this.mergedPresets); - - const opts: WaveAIOptsType = { - model: mergedPresets["ai:model"] ?? null, - apitype: mergedPresets["ai:apitype"] ?? null, - orgid: mergedPresets["ai:orgid"] ?? null, - apitoken: mergedPresets["ai:apitoken"] ?? null, - apiversion: mergedPresets["ai:apiversion"] ?? null, - maxtokens: mergedPresets["ai:maxtokens"] ?? null, - timeoutms: mergedPresets["ai:timeoutms"] ?? 60000, - baseurl: mergedPresets["ai:baseurl"] ?? null, - proxyurl: mergedPresets["ai:proxyurl"] ?? null, - }; - return opts; - }); - - this.viewText = atom((get) => { - const viewTextChildren: HeaderElem[] = []; - const aiOpts = get(this.aiOpts); - const presets = get(this.presetMap); - const presetKey = get(this.presetKey); - const presetName = presets[presetKey]?.["display:name"] ?? ""; - const isCloud = isBlank(aiOpts.apitoken) && isBlank(aiOpts.baseurl); - - // Handle known API providers - switch (aiOpts?.apitype) { - case "anthropic": - viewTextChildren.push({ - elemtype: "iconbutton", - icon: "globe", - title: `Using Remote Anthropic API (${aiOpts.model})`, - noAction: true, - }); - break; - case "perplexity": - viewTextChildren.push({ - elemtype: "iconbutton", - icon: "globe", - title: `Using Remote Perplexity API (${aiOpts.model})`, - noAction: true, - }); - break; - default: - if (isCloud) { - viewTextChildren.push({ - elemtype: "iconbutton", - icon: "cloud", - title: "Using Wave's AI Proxy (gpt-5-mini)", - noAction: true, - }); - } else { - const baseUrl = aiOpts.baseurl ?? "OpenAI Default Endpoint"; - const modelName = aiOpts.model; - if (baseUrl.startsWith("http://localhost") || baseUrl.startsWith("http://127.0.0.1")) { - viewTextChildren.push({ - elemtype: "iconbutton", - icon: "location-dot", - title: `Using Local Model @ ${baseUrl} (${modelName})`, - noAction: true, - }); - } else { - viewTextChildren.push({ - elemtype: "iconbutton", - icon: "globe", - title: `Using Remote Model @ ${baseUrl} (${modelName})`, - noAction: true, - }); - } - } - } - - const dropdownItems = Object.entries(presets) - .sort((a, b) => ((a[1]["display:order"] ?? 0) > (b[1]["display:order"] ?? 0) ? 1 : -1)) - .map( - (preset) => - ({ - label: preset[1]["display:name"], - onClick: () => - fireAndForget(() => - ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { - "ai:preset": preset[0], - }) - ), - }) as MenuItem - ); - dropdownItems.push({ - label: "Add AI preset...", - onClick: () => { - fireAndForget(async () => { - const path = `${getApi().getConfigDir()}/presets/ai.json`; - const blockDef: BlockDef = { - meta: { - view: "preview", - file: path, - }, - }; - await createBlock(blockDef, false, true); - }); - }, - }); - viewTextChildren.push({ - elemtype: "menubutton", - text: presetName, - title: "Select AI Configuration", - items: dropdownItems, - }); - return viewTextChildren; - }); - this.endIconButtons = atom((_) => { - let clearButton: IconButtonDecl = { - elemtype: "iconbutton", - icon: "delete-left", - title: "Clear Chat History", - click: this.clearMessages.bind(this), - }; - return [clearButton]; - }); - } + viewType = "waveai"; + viewIcon = atom("sparkles"); + viewName = atom("Wave AI"); + noPadding = atom(true); + viewComponent = WaveAiDeprecatedView; - get viewComponent(): ViewComponent { - return WaveAi; - } - - dispose() { - DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId)); - } - - async populateMessages(): Promise { - const history = await this.fetchAiData(); - globalStore.set(this.messagesAtom, history.map(promptToMsg)); - } - - async fetchAiData(): Promise> { - const { data } = await fetchWaveFile(this.blockId, "aidata"); - if (!data) { - return []; - } - const history: Array = JSON.parse(new TextDecoder().decode(data)); - return history.slice(Math.max(history.length - slidingWindowSize, 0)); - } - - giveFocus(): boolean { - if (this?.textAreaRef?.current) { - this.textAreaRef.current?.focus(); - return true; - } - return false; - } - - getAiName(): string { - const blockMeta = globalStore.get(this.blockAtom)?.meta ?? {}; - const settings = globalStore.get(atoms.settingsAtom) ?? {}; - const name = blockMeta["ai:name"] ?? settings["ai:name"] ?? null; - return name; - } - - setLocked(locked: boolean) { - globalStore.set(this.locked, locked); - } - - sendMessage(text: string, user: string = "user") { - const clientId = ClientModel.getInstance().clientId; - this.setLocked(true); - - const newMessage: ChatMessageType = { - id: crypto.randomUUID(), - user, - text, - }; - globalStore.set(this.addMessageAtom, newMessage); - // send message to backend and get response - const opts = globalStore.get(this.aiOpts); - const newPrompt: WaveAIPromptMessageType = { - role: "user", - content: text, - }; - const handleAiStreamingResponse = async () => { - const typingMessage: ChatMessageType = { - id: crypto.randomUUID(), - user: "assistant", - text: "", - }; - - // Add a typing indicator - globalStore.set(this.addMessageAtom, typingMessage); - const history = await this.fetchAiData(); - const beMsg: WaveAIStreamRequest = { - clientid: clientId, - opts: opts, - prompt: [...history, newPrompt], - }; - let fullMsg = ""; - try { - const aiGen = RpcApi.StreamWaveAiCommand(TabRpcClient, beMsg, { timeout: opts.timeoutms }); - for await (const msg of aiGen) { - fullMsg += msg.text ?? ""; - globalStore.set(this.updateLastMessageAtom, msg.text ?? "", true); - if (this.cancel) { - break; - } - } - if (fullMsg == "") { - // remove a message if empty - globalStore.set(this.removeLastMessageAtom); - // only save the author's prompt - await BlockService.SaveWaveAiData(this.blockId, [...history, newPrompt]); - } else { - const responsePrompt: WaveAIPromptMessageType = { - role: "assistant", - content: fullMsg, - }; - //mark message as complete - globalStore.set(this.updateLastMessageAtom, "", false); - // save a complete message prompt and response - await BlockService.SaveWaveAiData(this.blockId, [...history, newPrompt, responsePrompt]); - } - } catch (error) { - const updatedHist = [...history, newPrompt]; - if (fullMsg == "") { - globalStore.set(this.removeLastMessageAtom); - } else { - globalStore.set(this.updateLastMessageAtom, "", false); - const responsePrompt: WaveAIPromptMessageType = { - role: "assistant", - content: fullMsg, - }; - updatedHist.push(responsePrompt); - } - const errMsg: string = (error as Error).message; - const errorMessage: ChatMessageType = { - id: crypto.randomUUID(), - user: "error", - text: errMsg, - }; - globalStore.set(this.addMessageAtom, errorMessage); - globalStore.set(this.updateLastMessageAtom, "", false); - const errorPrompt: WaveAIPromptMessageType = { - role: "error", - content: errMsg, - }; - updatedHist.push(errorPrompt); - await BlockService.SaveWaveAiData(this.blockId, updatedHist); - } - this.setLocked(false); - this.cancel = false; - }; - fireAndForget(handleAiStreamingResponse); - } - - useWaveAi() { - return { - sendMessage: this.sendMessage.bind(this) as (text: string) => void, - }; - } - - async clearMessages() { - await BlockService.SaveWaveAiData(this.blockId, []); - globalStore.set(this.messagesAtom, []); - } - - keyDownHandler(waveEvent: WaveKeyboardEvent): boolean { - if (checkKeyPressed(waveEvent, "Cmd:l")) { - fireAndForget(this.clearMessages.bind(this)); - return true; - } - return false; - } -} - -const ChatItem = ({ chatItemAtom, model }: ChatItemProps) => { - const chatItem = useAtomValue(chatItemAtom); - const { user, text } = chatItem; - const fontSize = useAtomValue(model.mergedPresets)?.["ai:fontsize"]; - const fixedFontSize = useAtomValue(model.mergedPresets)?.["ai:fixedfontsize"]; - const renderContent = useMemo(() => { - if (user == "error") { - return ( - <> -
-
- -
-
-
- -
- - ); - } - if (user == "assistant") { - return text ? ( - <> -
-
- -
-
-
- -
- - ) : ( - <> -
- -
- - - ); - } - return ( - <> -
- -
- - ); - }, [text, user, fontSize, fixedFontSize]); - - return
{renderContent}
; -}; - -interface ChatWindowProps { - chatWindowRef: React.RefObject; - msgWidths: object; - model: WaveAiModel; + constructor(_: ViewModelInitType) {} } -const ChatWindow = memo( - forwardRef(({ chatWindowRef, msgWidths, model }, ref) => { - const isUserScrolling = useRef(false); - const osRef = useRef(null); - 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()) { - 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(() => { - 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(); - if (viewport.scrollTop > chatWindowRef.current?.clientHeight - viewport.clientHeight - 100) { - isUserScrolling.current = false; - } - }), - [] - ); - - const handleUserScroll = useCallback( - throttle(100, () => { - isUserScrolling.current = true; - determineUnsetScroll(); - }), - [] - ); - - useEffect(() => { - if (osRef.current?.osInstance()) { - const { viewport } = osRef.current.osInstance().elements(); - - viewport.addEventListener("wheel", handleUserScroll, { passive: true }); - viewport.addEventListener("touchmove", handleUserScroll, { passive: true }); - - return () => { - viewport.removeEventListener("wheel", handleUserScroll); - viewport.removeEventListener("touchmove", handleUserScroll); - if (osRef.current && osRef.current.osInstance()) { - osRef.current.osInstance().destroy(); - } - }; - } - }, []); - - const handleScrollbarInitialized = (instance: OverlayScrollbars) => { - const { viewport } = instance.elements(); - viewport.removeAttribute("tabindex"); - viewport.scrollTo({ - behavior: "auto", - top: chatWindowRef.current?.scrollHeight || 0, - }); - }; - - const handleScrollbarUpdated = (instance: OverlayScrollbars) => { - const { viewport } = instance.elements(); - viewport.removeAttribute("tabindex"); - }; - - return ( - -
-
- {splitMessages.map((chitem, idx) => ( - - ))} -
-
- ); - }) -); - -interface ChatInputProps { - value: string; - baseFontSize: number; - onChange: (e: React.ChangeEvent) => void; - onKeyDown: (e: React.KeyboardEvent) => void; - onMouseDown: (e: React.MouseEvent) => void; - model: WaveAiModel; -} - -const ChatInput = forwardRef( - ({ value, onChange, onKeyDown, onMouseDown, baseFontSize, model }, ref) => { - const textAreaRef = useRef(null); - - useImperativeHandle(ref, () => textAreaRef.current as HTMLTextAreaElement); - - useEffect(() => { - model.textAreaRef = textAreaRef; - }, []); - - const adjustTextAreaHeight = useCallback( - (value: string) => { - if (textAreaRef.current == null) { - return; - } - - // Adjust the height of the textarea to fit the text - const textAreaMaxLines = 5; - const textAreaLineHeight = baseFontSize * 1.5; - const textAreaMinHeight = textAreaLineHeight; - const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines; - - if (value === "") { - textAreaRef.current.style.height = `${textAreaLineHeight}px`; - return; - } - - textAreaRef.current.style.height = `${textAreaLineHeight}px`; - const scrollHeight = textAreaRef.current.scrollHeight; - const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight); - textAreaRef.current.style.height = newHeight + "px"; - }, - [baseFontSize] - ); - - useEffect(() => { - adjustTextAreaHeight(value); - }, [value]); - - return ( - - ); - } -); - -const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => { - const { sendMessage } = model.useWaveAi(); - const waveaiRef = useRef(null); - const chatWindowRef = useRef(null); - const osRef = useRef(null); - const inputRef = useRef(null); - - const [value, setValue] = useState(""); - const [selectedBlockIdx, setSelectedBlockIdx] = useState(null); - - const baseFontSize: number = 14; - const msgWidths = {}; - const locked = useAtomValue(model.locked); - const aiOpts = useAtomValue(model.aiOpts); - const isUsingProxy = isBlank(aiOpts.apitoken) && isBlank(aiOpts.baseurl); - - // a weird workaround to initialize ansynchronously - useEffect(() => { - fireAndForget(model.populateMessages.bind(model)); - }, []); - - const handleTextAreaChange = (e: React.ChangeEvent) => { - setValue(e.target.value); - }; - - const updatePreTagOutline = (clickedPre?: HTMLElement | null) => { - const pres = chatWindowRef.current?.querySelectorAll("pre"); - if (!pres) return; - - pres.forEach((preElement, idx) => { - if (preElement === clickedPre) { - setSelectedBlockIdx(idx); - } else { - preElement.style.outline = "none"; - } - }); - - if (clickedPre) { - clickedPre.style.outline = outline; - } - }; - - useEffect(() => { - if (selectedBlockIdx !== null) { - const pres = chatWindowRef.current?.querySelectorAll("pre"); - if (pres && pres[selectedBlockIdx]) { - pres[selectedBlockIdx].style.outline = outline; - } - } - }, [selectedBlockIdx]); - - const handleTextAreaMouseDown = () => { - updatePreTagOutline(); - setSelectedBlockIdx(null); - }; - - const handleEnterKeyPressed = useCallback(() => { - // using globalStore to avoid potential timing problems - // useAtom means the component must rerender once before - // the unlock is detected. this automatically checks on the - // callback firing instead - const locked = globalStore.get(model.locked); - if (locked || value === "") return; - - sendMessage(value); - setValue(""); - setSelectedBlockIdx(null); - }, [value]); - - const updateScrollTop = () => { - const pres = chatWindowRef.current?.querySelectorAll("pre"); - if (!pres || selectedBlockIdx === null) return; - - const block = pres[selectedBlockIdx]; - if (!block || !osRef.current?.osInstance()) return; - - const { viewport, scrollOffsetElement } = osRef.current.osInstance().elements(); - const chatWindowTop = scrollOffsetElement.scrollTop; - const chatWindowHeight = chatWindowRef.current.clientHeight; - const chatWindowBottom = chatWindowTop + chatWindowHeight; - const elemTop = block.offsetTop; - const elemBottom = elemTop + block.offsetHeight; - const elementIsInView = elemBottom <= chatWindowBottom && elemTop >= chatWindowTop; - - if (!elementIsInView) { - let scrollPosition; - if (elemBottom > chatWindowBottom) { - scrollPosition = elemTop - chatWindowHeight + block.offsetHeight + 15; - } else if (elemTop < chatWindowTop) { - scrollPosition = elemTop - 15; - } - viewport.scrollTo({ - behavior: "auto", - top: scrollPosition, - }); - } - }; - - const shouldSelectCodeBlock = (key: "ArrowUp" | "ArrowDown") => { - const textarea = inputRef.current; - const cursorPosition = textarea?.selectionStart || 0; - const textBeforeCursor = textarea?.value.slice(0, cursorPosition) || ""; - - return ( - (textBeforeCursor.indexOf("\n") === -1 && cursorPosition === 0 && key === "ArrowUp") || - selectedBlockIdx !== null - ); - }; - - const handleArrowUpPressed = (e: React.KeyboardEvent) => { - if (shouldSelectCodeBlock("ArrowUp")) { - e.preventDefault(); - const pres = chatWindowRef.current?.querySelectorAll("pre"); - let blockIndex = selectedBlockIdx; - if (!pres) return; - if (blockIndex === null) { - setSelectedBlockIdx(pres.length - 1); - } else if (blockIndex > 0) { - blockIndex--; - setSelectedBlockIdx(blockIndex); - } - updateScrollTop(); - } - }; - - const handleArrowDownPressed = (e: React.KeyboardEvent) => { - if (shouldSelectCodeBlock("ArrowDown")) { - e.preventDefault(); - const pres = chatWindowRef.current?.querySelectorAll("pre"); - let blockIndex = selectedBlockIdx; - if (!pres) return; - if (blockIndex === null) return; - if (blockIndex < pres.length - 1 && blockIndex >= 0) { - setSelectedBlockIdx(++blockIndex); - updateScrollTop(); - } else { - inputRef.current.focus(); - setSelectedBlockIdx(null); - } - updateScrollTop(); - } - }; - - const handleTextAreaKeyDown = (e: React.KeyboardEvent) => { - const waveEvent = adaptFromReactOrNativeKeyEvent(e); - if (checkKeyPressed(waveEvent, "Enter")) { - e.preventDefault(); - handleEnterKeyPressed(); - } else if (checkKeyPressed(waveEvent, "ArrowUp")) { - handleArrowUpPressed(e); - } else if (checkKeyPressed(waveEvent, "ArrowDown")) { - handleArrowDownPressed(e); - } - }; - - let buttonClass = "waveai-submit-button"; - let buttonIcon = makeIconClass("arrow-up", false); - let buttonTitle = "run"; - if (locked) { - buttonClass = "waveai-submit-button stop"; - buttonIcon = makeIconClass("stop", false); - buttonTitle = "stop"; - } - const handleButtonPress = useCallback(() => { - if (locked) { - model.cancel = true; - } else { - handleEnterKeyPressed(); - } - }, [locked, handleEnterKeyPressed]); - +function WaveAiDeprecatedView() { const handleOpenAIPanel = useCallback(() => { WorkspaceLayoutModel.getInstance().setAIPanelVisible(true); }, []); return ( -
- {isUsingProxy && ( -
- - - Wave AI Proxy is deprecated and will be removed. Please use the new{" "} - {" "} - instead (better model, terminal integration, tool support, image uploads). - -
- )} -
- -
-
-
- -
-
+
); -}; - -export { WaveAi }; +} diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index f3043e6d98..f11eca91da 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -373,7 +373,6 @@ const Widgets = memo(() => { const fullConfig = useAtomValue(env.atoms.fullConfigAtom); const hasConfigErrors = useAtomValue(env.atoms.hasConfigErrors); const workspaceId = useAtomValue(env.atoms.workspaceId); - const hasCustomAIPresets = useAtomValue(env.atoms.hasCustomAIPresetsAtom); const [mode, setMode] = useState<"normal" | "compact" | "supercompact">("normal"); const containerRef = useRef(null); const measurementRef = useRef(null); @@ -381,12 +380,7 @@ const Widgets = memo(() => { const featureWaveAppBuilder = fullConfig?.settings?.["feature:waveappbuilder"] ?? false; const widgetsMap = fullConfig?.widgets ?? {}; const filteredWidgets = Object.fromEntries( - Object.entries(widgetsMap).filter(([key, widget]) => { - if (!hasCustomAIPresets && key === "defwidget@ai") { - return false; - } - return shouldIncludeWidgetForWorkspace(widget, workspaceId); - }) + Object.entries(widgetsMap).filter(([_key, widget]) => shouldIncludeWidgetForWorkspace(widget, workspaceId)) ); const widgets = sortByDisplayOrder(filteredWidgets); diff --git a/frontend/preview/previews/waveai.preview.tsx b/frontend/preview/previews/waveai.preview.tsx new file mode 100644 index 0000000000..1d5003f0d4 --- /dev/null +++ b/frontend/preview/previews/waveai.preview.tsx @@ -0,0 +1,53 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Block } from "@/app/block/block"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; +import * as React from "react"; +import { makeMockNodeModel } from "../mock/mock-node-model"; + +const PreviewNodeId = "preview-waveai-node"; + +export default function WaveAIPreview() { + const env = useWaveEnv(); + const [blockId, setBlockId] = React.useState(null); + + React.useEffect(() => { + env.createBlock( + { + meta: { + view: "waveai", + }, + }, + false, + false + ).then((id) => setBlockId(id)); + }, [env]); + + const nodeModel = React.useMemo( + () => + blockId == null + ? null + : makeMockNodeModel({ + nodeId: PreviewNodeId, + blockId, + innerRect: { width: "900px", height: "480px" }, + }), + [blockId] + ); + + if (blockId == null || nodeModel == null) { + return null; + } + + return ( +
+
full deprecated waveai block with the FE-only replacement UI
+
+
+ +
+
+
+ ); +} diff --git a/frontend/preview/previews/widgets.preview.tsx b/frontend/preview/previews/widgets.preview.tsx index 440ae03a6a..4b82314510 100644 --- a/frontend/preview/previews/widgets.preview.tsx +++ b/frontend/preview/previews/widgets.preview.tsx @@ -59,20 +59,12 @@ const mockWidgets: { [key: string]: WidgetConfigType } = { "display:order": 2, blockdef: { meta: { view: "web", url: "https://waveterm.dev" } }, }, - "defwidget@ai": { - icon: "sparkles", - color: "#a78bfa", - label: "AI", - description: "Open Wave AI", - "display:order": 3, - blockdef: { meta: { view: "waveai" } }, - }, "defwidget@files": { icon: "folder", color: "#fbbf24", label: "Files", description: "Open file browser", - "display:order": 4, + "display:order": 3, blockdef: { meta: { view: "preview", connection: "local" } }, }, "defwidget@sysinfo": { @@ -80,7 +72,7 @@ const mockWidgets: { [key: string]: WidgetConfigType } = { color: "#34d399", label: "Sysinfo", description: "Open system info", - "display:order": 5, + "display:order": 4, blockdef: { meta: { view: "sysinfo" } }, }, }; @@ -90,7 +82,6 @@ const fullConfigAtom = atom({ settings: {}, widgets: mockWidgets function makeWidgetsEnv( baseEnv: WaveEnv, isDev: boolean, - hasCustomAIPresets: boolean, apps?: AppInfo[], atomOverrides?: Partial ) { @@ -99,7 +90,6 @@ function makeWidgetsEnv( rpc: { ListAllAppsCommand: () => Promise.resolve(apps ?? []) }, atoms: { fullConfigAtom, - hasCustomAIPresetsAtom: atom(hasCustomAIPresets), ...atomOverrides, }, }); @@ -108,20 +98,18 @@ function makeWidgetsEnv( function WidgetsScenario({ label, isDev = false, - hasCustomAIPresets = true, height, apps, }: { label: string; isDev?: boolean; - hasCustomAIPresets?: boolean; height?: number; apps?: AppInfo[]; }) { const baseEnv = useWaveEnv(); const envRef = useRef(null); if (envRef.current == null) { - envRef.current = makeWidgetsEnv(baseEnv, isDev, hasCustomAIPresets, apps, { + envRef.current = makeWidgetsEnv(baseEnv, isDev, apps, { hasConfigErrors: hasConfigErrorsAtom, }); } @@ -149,7 +137,7 @@ function WidgetsResizable({ isDev }: { isDev: boolean }) { const baseEnv = useWaveEnv(); const envRef = useRef(null); if (envRef.current == null) { - envRef.current = makeWidgetsEnv(baseEnv, isDev, true, mockApps, { hasConfigErrors: hasConfigErrorsAtom }); + envRef.current = makeWidgetsEnv(baseEnv, isDev, mockApps, { hasConfigErrors: hasConfigErrorsAtom }); } return ( @@ -224,8 +212,7 @@ export function WidgetsPreview() {
- - +
diff --git a/pkg/wconfig/defaultconfig/widgets.json b/pkg/wconfig/defaultconfig/widgets.json index 97a3d26c10..2d0524b7dd 100644 --- a/pkg/wconfig/defaultconfig/widgets.json +++ b/pkg/wconfig/defaultconfig/widgets.json @@ -31,18 +31,8 @@ } } }, - "defwidget@ai": { - "display:order": -2, - "icon": "sparkles", - "label": "ai", - "blockdef": { - "meta": { - "view": "waveai" - } - } - }, "defwidget@sysinfo": { - "display:order": -1, + "display:order": -2, "icon": "chart-line", "label": "sysinfo", "blockdef": { From b26eb69df620d4288a153ff6e464a0fdaa804ac2 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Fri, 27 Mar 2026 01:45:10 +0800 Subject: [PATCH 05/11] fix: set XDG_CONFIG_DIRS and XDG_DATA_DIRS defaults in tryGetPamEnvVars (#3121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fixes #2970: WaveTerm does not inherit `XDG_CONFIG_DIRS` (and `XDG_DATA_DIRS`) when snap or other environments strip these variables and PAM env files do not define them - In `tryGetPamEnvVars()`, after the existing `XDG_RUNTIME_DIR` fallback, adds identical fallback logic for `XDG_CONFIG_DIRS` (default: `/etc/xdg`) and `XDG_DATA_DIRS` (default: `/usr/local/share:/usr/share`) per the XDG Base Directory Specification - No behavior change when these vars are already set by PAM env files ## Root Cause Snap confinement strips several XDG environment variables. `tryGetPamEnvVars()` already handles `XDG_RUNTIME_DIR` with a sensible default, but `XDG_CONFIG_DIRS` and `XDG_DATA_DIRS` were left unhandled, causing child shells to receive empty/unset values. ## Test plan - [ ] `gofmt -l ./pkg/shellexec/` — no output (clean) - [ ] `go build ./...` — succeeds - [ ] On Linux with snap, verify that child shells receive `XDG_CONFIG_DIRS=/etc/xdg` and `XDG_DATA_DIRS=/usr/local/share:/usr/share` when the variables are not set by the desktop environment Signed-off-by: majiayu000 <1835304752@qq.com> --- pkg/shellexec/shellexec.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index 4850eee1b8..35af5446a3 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -760,5 +760,11 @@ func tryGetPamEnvVars() map[string]string { if runtime_dir, ok := envVars["XDG_RUNTIME_DIR"]; !ok || runtime_dir == "" { envVars["XDG_RUNTIME_DIR"] = "/run/user/" + fmt.Sprint(os.Getuid()) } + if configDirs, ok := envVars["XDG_CONFIG_DIRS"]; !ok || configDirs == "" { + envVars["XDG_CONFIG_DIRS"] = "/etc/xdg" + } + if dataDirs, ok := envVars["XDG_DATA_DIRS"]; !ok || dataDirs == "" { + envVars["XDG_DATA_DIRS"] = "/usr/local/share:/usr/share" + } return envVars } From f92a953e07b2188d6dbf7479ed95f7860cccfd90 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Thu, 26 Mar 2026 10:45:33 -0700 Subject: [PATCH 06/11] v0.14.4 release notes / onboarding (#3120) --- docs/docs/releasenotes.mdx | 24 ++++++ frontend/app/onboarding/onboarding-common.tsx | 2 +- .../onboarding/onboarding-upgrade-patch.tsx | 7 ++ .../onboarding/onboarding-upgrade-v0144.tsx | 84 +++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 frontend/app/onboarding/onboarding-upgrade-v0144.tsx diff --git a/docs/docs/releasenotes.mdx b/docs/docs/releasenotes.mdx index adc2ce6fe7..421f212731 100644 --- a/docs/docs/releasenotes.mdx +++ b/docs/docs/releasenotes.mdx @@ -6,6 +6,30 @@ sidebar_position: 200 # Release Notes +### v0.14.4 — Mar 26, 2026 + +Wave v0.14.4 introduces vertical tabs, upgrades to xterm.js v6, and includes a collection of bug fixes and internal improvements. + +**Vertical Tab Bar:** + +- **New Vertical Tab Bar Option** - Tabs can now be displayed vertically along the side of the window, giving you more horizontal space and easier access to tabs when you have many open. Toggle between horizontal and vertical tab layouts in settings. + +**Terminal Improvements:** + +- **xterm.js v6.0.0 Upgrade** - Upgraded to the latest xterm.js v6, bringing improved terminal compatibility and rendering. This should resolve various terminal rendering quirks observed with tools like Claude Code. + +**Other Changes:** + +- **`backgrounds.json`** - Renamed `presets/bg.json` to `backgrounds.json` and moved background config to new `tab:background` key (auto-migrated on startup) +- **Config Errors Moved** - Config errors removed from the tab bar and moved to Settings / WaveConfig view for less clutter +- **Warn on Unsaved Changes** - WaveConfig view now warns before discarding unsaved changes +- **Stream Performance** - Migrated file streaming to new modern interface with flow control, fixing a large time-to-first-byte streaming bug +- **macOS First Click** - Improved first-click handling on macOS (cancel the click but properly set block/WaveAI focus) +- Deprecated legacy AI widget has been removed +- [bugfix] Fixed focus bug for newly created blocks +- Electron upgraded to v41 +- Package updates and dependency upgrades + ### v0.14.2 — Mar 12, 2026 Wave v0.14.2 adds block/tab badges, directory preview improvements, and assorted bug fixes. diff --git a/frontend/app/onboarding/onboarding-common.tsx b/frontend/app/onboarding/onboarding-common.tsx index 60711746e1..96e49b1a79 100644 --- a/frontend/app/onboarding/onboarding-common.tsx +++ b/frontend/app/onboarding/onboarding-common.tsx @@ -1,7 +1,7 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -export const CurrentOnboardingVersion = "v0.14.3"; +export const CurrentOnboardingVersion = "v0.14.4"; export function OnboardingGradientBg() { return ( diff --git a/frontend/app/onboarding/onboarding-upgrade-patch.tsx b/frontend/app/onboarding/onboarding-upgrade-patch.tsx index 0eded88f12..c3dd5004a2 100644 --- a/frontend/app/onboarding/onboarding-upgrade-patch.tsx +++ b/frontend/app/onboarding/onboarding-upgrade-patch.tsx @@ -25,6 +25,7 @@ import { UpgradeOnboardingModal_v0_13_1_Content } from "./onboarding-upgrade-v01 import { UpgradeOnboardingModal_v0_14_0_Content } from "./onboarding-upgrade-v0140"; import { UpgradeOnboardingModal_v0_14_1_Content } from "./onboarding-upgrade-v0141"; import { UpgradeOnboardingModal_v0_14_2_Content } from "./onboarding-upgrade-v0142"; +import { UpgradeOnboardingModal_v0_14_4_Content } from "./onboarding-upgrade-v0144"; interface VersionConfig { version: string; @@ -139,6 +140,12 @@ export const UpgradeOnboardingVersions: VersionConfig[] = [ version: "v0.14.3", content: () => , prevText: "Prev (v0.14.1)", + nextText: "Next (v0.14.4)", + }, + { + version: "v0.14.4", + content: () => , + prevText: "Prev (v0.14.3)", }, ]; diff --git a/frontend/app/onboarding/onboarding-upgrade-v0144.tsx b/frontend/app/onboarding/onboarding-upgrade-v0144.tsx new file mode 100644 index 0000000000..6fc50f8919 --- /dev/null +++ b/frontend/app/onboarding/onboarding-upgrade-v0144.tsx @@ -0,0 +1,84 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +const UpgradeOnboardingModal_v0_14_4_Content = () => { + return ( +
+
+

+ Wave v0.14.4 introduces vertical tabs, upgrades to xterm.js v6, and includes bug fixes and UI + improvements. +

+
+ +
+
+ +
+
+
Vertical Tab Bar
+
+
    +
  • + New Vertical Tab Bar Option - Tabs can now be displayed vertically + along the side of the window for more horizontal space. Toggle between horizontal and + vertical layouts in settings. +
  • +
+
+
+
+ +
+
+ +
+
+
Terminal Improvements
+
+
    +
  • + xterm.js v6.0.0 Upgrade - Improved terminal compatibility and + rendering, resolving quirks with tools like Claude Code +
  • +
+
+
+
+ +
+
+ +
+
+
Other Changes
+
+
    +
  • + macOS First Click - First click now focuses the clicked widget +
  • +
  • + + backgrounds.json + {" "} + - Renamed presets/bg.json to backgrounds.json +
  • +
  • + Config Errors Moved - Config errors to the WaveConfig view for less + clutter +
  • +
  • WaveConfig now warns on Unsaved Changes
  • +
  • Preview streaming fixes for images/videos
  • +
  • Deprecated legacy AI widget has been removed
  • +
  • [bugfix] Fixed focus bug for newly created blocks
  • +
+
+
+
+
+ ); +}; + +UpgradeOnboardingModal_v0_14_4_Content.displayName = "UpgradeOnboardingModal_v0_14_4_Content"; + +export { UpgradeOnboardingModal_v0_14_4_Content }; From 889e6287800f7c20390ae4756619f9a0d863e9c9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:59:46 -0700 Subject: [PATCH 07/11] Show Claude icon in terminal header while Claude Code is active (#3046) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This updates the terminal shell-integration badge so it reflects Claude Code activity instead of always rendering the generic AI icon. When the active shell command is Claude Code, the header now shows the Claude logo. - **Terminal shell-integration badge** - Updated `getShellIntegrationIconButton()` to render the Claude logo while Claude Code is the active running command. - Kept the existing shell-integration states and messaging intact for non-Claude commands. - **Claude Code detection** - Added command detection for Claude Code in the OSC shell-integration flow. - Tracks active Claude sessions on `TermWrap`, including initial runtime-info hydration and command lifecycle transitions. - Handles common invocation forms, including direct binary paths and commands wrapped by env var assignments / `env`. - **UI rendering** - Added `@lobehub/icons` and used its `Claude` icon in the terminal header path. - Reused the existing icon-button rendering contract by passing a React node for the icon where needed. - **Focused coverage** - Added a small unit test for Claude command detection to lock in the supported command forms. ```ts const claudeCodeActive = get(this.termRef.current.claudeCodeActiveAtom); const icon = claudeCodeActive ? React.createElement(TermClaudeIcon) : "sparkles"; ``` - **screenshot** - ![Claude terminal header badge](https://github.com/user-attachments/assets/4b53f671-8432-4878-b2d2-e3afeba7814f) --- 💬 Send tasks to Copilot coding agent from [Slack](https://gh.io/cca-slack-docs) and [Teams](https://gh.io/cca-teams-docs) to turn conversations into code. Copilot posts an update in your thread when it's finished. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> Co-authored-by: sawka --- frontend/app/asset/claude-color.svg | 1 + frontend/app/view/term/osc-handlers.test.ts | 23 ++++++ frontend/app/view/term/osc-handlers.ts | 36 ++++++++-- frontend/app/view/term/term-model.ts | 14 ++-- frontend/app/view/term/term.tsx | 19 +++-- frontend/app/view/term/termwrap.ts | 48 +++++++++++-- package-lock.json | 80 ++++++++++----------- 7 files changed, 159 insertions(+), 62 deletions(-) create mode 100644 frontend/app/asset/claude-color.svg create mode 100644 frontend/app/view/term/osc-handlers.test.ts diff --git a/frontend/app/asset/claude-color.svg b/frontend/app/asset/claude-color.svg new file mode 100644 index 0000000000..b70e167740 --- /dev/null +++ b/frontend/app/asset/claude-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/view/term/osc-handlers.test.ts b/frontend/app/view/term/osc-handlers.test.ts new file mode 100644 index 0000000000..3955a2bc8f --- /dev/null +++ b/frontend/app/view/term/osc-handlers.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { isClaudeCodeCommand } from "./osc-handlers"; + +describe("isClaudeCodeCommand", () => { + it("matches direct Claude Code invocations", () => { + expect(isClaudeCodeCommand("claude")).toBe(true); + expect(isClaudeCodeCommand("claude --dangerously-skip-permissions")).toBe(true); + }); + + it("matches Claude Code invocations wrapped with env assignments", () => { + expect(isClaudeCodeCommand('ANTHROPIC_API_KEY="test" claude')).toBe(true); + expect(isClaudeCodeCommand("env FOO=bar claude --print")).toBe(true); + }); + + it("ignores other commands", () => { + expect(isClaudeCodeCommand("claudes")).toBe(false); + expect(isClaudeCodeCommand("echo claude")).toBe(false); + expect(isClaudeCodeCommand("ls ~/claude")).toBe(false); + expect(isClaudeCodeCommand("cat /logs/claude")).toBe(false); + expect(isClaudeCodeCommand("")).toBe(false); + }); +}); diff --git a/frontend/app/view/term/osc-handlers.ts b/frontend/app/view/term/osc-handlers.ts index f44659d2c6..7fe7dcd4ee 100644 --- a/frontend/app/view/term/osc-handlers.ts +++ b/frontend/app/view/term/osc-handlers.ts @@ -25,6 +25,8 @@ const Osc52MaxRawLength = 128 * 1024; // includes selector + base64 + whitespace // See aiprompts/wave-osc-16162.md for full documentation export type ShellIntegrationStatus = "ready" | "running-command"; +const ClaudeCodeRegex = /^claude\b/; + type Osc16162Command = | { command: "A"; data: Record } | { command: "C"; data: { cmd64?: string } } @@ -43,41 +45,56 @@ type Osc16162Command = | { command: "I"; data: { inputempty?: boolean } } | { command: "R"; data: Record }; +function normalizeCmd(decodedCmd: string): string { + let normalizedCmd = decodedCmd.trim(); + normalizedCmd = normalizedCmd.replace(/^env\s+/, ""); + normalizedCmd = normalizedCmd.replace(/^(?:\w+=(?:"[^"]*"|'[^']*'|\S+)\s+)*/, ""); + return normalizedCmd; +} + function checkCommandForTelemetry(decodedCmd: string) { if (!decodedCmd) { return; } - if (decodedCmd.startsWith("ssh ")) { + const normalizedCmd = normalizeCmd(decodedCmd); + + if (normalizedCmd.startsWith("ssh ")) { recordTEvent("conn:connect", { "conn:conntype": "ssh-manual" }); return; } const editorsRegex = /^(vim|vi|nano|nvim)\b/; - if (editorsRegex.test(decodedCmd)) { + if (editorsRegex.test(normalizedCmd)) { recordTEvent("action:term", { "action:type": "cli-edit" }); return; } const tailFollowRegex = /(^|\|\s*)tail\s+-[fF]\b/; - if (tailFollowRegex.test(decodedCmd)) { + if (tailFollowRegex.test(normalizedCmd)) { recordTEvent("action:term", { "action:type": "cli-tailf" }); return; } - const claudeRegex = /^claude\b/; - if (claudeRegex.test(decodedCmd)) { + if (ClaudeCodeRegex.test(normalizedCmd)) { recordTEvent("action:term", { "action:type": "claude" }); return; } const opencodeRegex = /^opencode\b/; - if (opencodeRegex.test(decodedCmd)) { + if (opencodeRegex.test(normalizedCmd)) { recordTEvent("action:term", { "action:type": "opencode" }); return; } } +export function isClaudeCodeCommand(decodedCmd: string): boolean { + if (!decodedCmd) { + return false; + } + return ClaudeCodeRegex.test(normalizeCmd(decodedCmd)); +} + function handleShellIntegrationCommandStart( termWrap: TermWrap, blockId: string, @@ -101,16 +118,20 @@ function handleShellIntegrationCommandStart( const decodedCmd = base64ToString(cmd.data.cmd64); rtInfo["shell:lastcmd"] = decodedCmd; globalStore.set(termWrap.lastCommandAtom, decodedCmd); + const isCC = isClaudeCodeCommand(decodedCmd); + globalStore.set(termWrap.claudeCodeActiveAtom, isCC); checkCommandForTelemetry(decodedCmd); } catch (e) { console.error("Error decoding cmd64:", e); rtInfo["shell:lastcmd"] = null; globalStore.set(termWrap.lastCommandAtom, null); + globalStore.set(termWrap.claudeCodeActiveAtom, false); } } } else { rtInfo["shell:lastcmd"] = null; globalStore.set(termWrap.lastCommandAtom, null); + globalStore.set(termWrap.claudeCodeActiveAtom, false); } rtInfo["shell:lastcmdexitcode"] = null; } @@ -287,6 +308,7 @@ export function handleOsc16162Command(data: string, blockId: string, loaded: boo case "A": { rtInfo["shell:state"] = "ready"; globalStore.set(termWrap.shellIntegrationStatusAtom, "ready"); + globalStore.set(termWrap.claudeCodeActiveAtom, false); const marker = terminal.registerMarker(0); if (marker) { termWrap.promptMarkers.push(marker); @@ -324,6 +346,7 @@ export function handleOsc16162Command(data: string, blockId: string, loaded: boo } break; case "D": + globalStore.set(termWrap.claudeCodeActiveAtom, false); if (cmd.data.exitcode != null) { rtInfo["shell:lastcmdexitcode"] = cmd.data.exitcode; } else { @@ -337,6 +360,7 @@ export function handleOsc16162Command(data: string, blockId: string, loaded: boo break; case "R": globalStore.set(termWrap.shellIntegrationStatusAtom, null); + globalStore.set(termWrap.claudeCodeActiveAtom, false); if (terminal.buffer.active.type === "alternate") { terminal.write("\x1b[?1049l"); } diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index f9ff44f325..bf77ef9535 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -10,7 +10,7 @@ import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; -import { TerminalView } from "@/app/view/term/term"; +import { TermClaudeIcon, TerminalView } from "@/app/view/term/term"; import { TermWshClient } from "@/app/view/term/term-wsh"; import { VDomModel } from "@/app/view/vdom/vdom-model"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; @@ -404,10 +404,12 @@ export class TermViewModel implements ViewModel { return null; } const shellIntegrationStatus = get(this.termRef.current.shellIntegrationStatusAtom); + const claudeCodeActive = get(this.termRef.current.claudeCodeActiveAtom); + const icon = claudeCodeActive ? React.createElement(TermClaudeIcon) : "sparkles"; if (shellIntegrationStatus == null) { return { elemtype: "iconbutton", - icon: "sparkles", + icon, className: "text-muted", title: "No shell integration — Wave AI unable to run commands.", noAction: true, @@ -416,14 +418,16 @@ export class TermViewModel implements ViewModel { if (shellIntegrationStatus === "ready") { return { elemtype: "iconbutton", - icon: "sparkles", + icon, className: "text-accent", title: "Shell ready — Wave AI can run commands in this terminal.", noAction: true, }; } if (shellIntegrationStatus === "running-command") { - let title = "Shell busy — Wave AI unable to run commands while another command is running."; + let title = claudeCodeActive + ? "Claude Code Detected" + : "Shell busy — Wave AI unable to run commands while another command is running."; if (this.termRef.current) { const inAltBuffer = this.termRef.current.terminal?.buffer?.active?.type === "alternate"; @@ -436,7 +440,7 @@ export class TermViewModel implements ViewModel { return { elemtype: "iconbutton", - icon: "sparkles", + icon, className: "text-warning", title: title, noAction: true, diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index e385894825..67eb5737c6 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -1,6 +1,7 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import ClaudeColorSvg from "@/app/asset/claude-color.svg"; import { SubBlock } from "@/app/block/block"; import type { BlockNodeModel } from "@/app/block/blocktypes"; import { NullErrorBoundary } from "@/app/element/errorboundary"; @@ -34,6 +35,16 @@ interface TerminalViewProps { model: TermViewModel; } +const TermClaudeIcon = React.memo(() => { + return ( + + ); +}); + +TermClaudeIcon.displayName = "TermClaudeIcon"; + const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) => { const connStatus = jotai.useAtomValue(model.connStatus); const [lastConnStatus, setLastConnStatus] = React.useState(connStatus); @@ -61,7 +72,7 @@ const TermVDomToolbarNode = ({ vdomBlockId, blockId, model }: TerminalViewProps const unsub = waveEventSubscribeSingle({ eventType: "blockclose", scope: WOS.makeORef("block", vdomBlockId), - handler: (event) => { + handler: (_event) => { RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("block", blockId), meta: { @@ -104,7 +115,7 @@ const TermVDomNodeSingleId = ({ vdomBlockId, blockId, model }: TerminalViewProps const unsub = waveEventSubscribeSingle({ eventType: "blockclose", scope: WOS.makeORef("block", vdomBlockId), - handler: (event) => { + handler: (_event) => { RpcApi.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("block", blockId), meta: { @@ -390,4 +401,4 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => ); }; -export { TerminalView }; +export { TermClaudeIcon, TerminalView }; diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 46211fd330..d79ce695cd 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import type { BlockNodeModel } from "@/app/block/blocktypes"; @@ -32,6 +32,7 @@ import { handleOsc16162Command, handleOsc52Command, handleOsc7Command, + isClaudeCodeCommand, type ShellIntegrationStatus, } from "./osc-handlers"; import { bufferLinesToText, createTempFileFromBlob, extractAllClipboardData, normalizeCursorStyle } from "./termutil"; @@ -92,6 +93,7 @@ export class TermWrap { promptMarkers: TermTypes.IMarker[] = []; shellIntegrationStatusAtom: jotai.PrimitiveAtom; lastCommandAtom: jotai.PrimitiveAtom; + claudeCodeActiveAtom: jotai.PrimitiveAtom; nodeModel: BlockNodeModel; // this can be null hoveredLinkUri: string | null = null; onLinkHover?: (uri: string | null, mouseX: number, mouseY: number) => void; @@ -131,6 +133,7 @@ export class TermWrap { this.promptMarkers = []; this.shellIntegrationStatusAtom = jotai.atom(null) as jotai.PrimitiveAtom; this.lastCommandAtom = jotai.atom(null) as jotai.PrimitiveAtom; + this.claudeCodeActiveAtom = jotai.atom(false); this.webglEnabledAtom = jotai.atom(false) as jotai.PrimitiveAtom; this.terminal = new Terminal(options); this.fitAddon = new FitAddon(); @@ -171,16 +174,34 @@ export class TermWrap { this.setTermRenderer(WebGLSupported && waveOptions.useWebGl ? "webgl" : "dom"); // Register OSC handlers this.terminal.parser.registerOscHandler(7, (data: string) => { - return handleOsc7Command(data, this.blockId, this.loaded); + try { + return handleOsc7Command(data, this.blockId, this.loaded); + } catch (e) { + console.error("[termwrap] osc 7 handler error", this.blockId, e); + return false; + } }); this.terminal.parser.registerOscHandler(52, (data: string) => { - return handleOsc52Command(data, this.blockId, this.loaded, this); + try { + return handleOsc52Command(data, this.blockId, this.loaded, this); + } catch (e) { + console.error("[termwrap] osc 52 handler error", this.blockId, e); + return false; + } }); this.terminal.parser.registerOscHandler(16162, (data: string) => { - return handleOsc16162Command(data, this.blockId, this.loaded, this); + try { + return handleOsc16162Command(data, this.blockId, this.loaded, this); + } catch (e) { + console.error("[termwrap] osc 16162 handler error", this.blockId, e); + return false; + } }); this.toDispose.push( this.terminal.parser.registerCsiHandler({ final: "J" }, (params) => { + if (params == null || params.length < 1) { + return false; + } if (params[0] === 3) { this.lastClearScrollbackTs = Date.now(); if (this.inSyncTransaction) { @@ -193,6 +214,9 @@ export class TermWrap { ); this.toDispose.push( this.terminal.parser.registerCsiHandler({ prefix: "?", final: "h" }, (params) => { + if (params == null || params.length < 1) { + return false; + } if (params[0] === 2026) { this.lastMode2026SetTs = Date.now(); this.inSyncTransaction = true; @@ -202,6 +226,9 @@ export class TermWrap { ); this.toDispose.push( this.terminal.parser.registerCsiHandler({ prefix: "?", final: "l" }, (params) => { + if (params == null || params.length < 1) { + return false; + } if (params[0] === 2026) { this.lastMode2026ResetTs = Date.now(); this.inSyncTransaction = false; @@ -345,16 +372,19 @@ export class TermWrap { const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { oref: WOS.makeORef("block", this.blockId), }); + let shellState: ShellIntegrationStatus = null; if (rtInfo && rtInfo["shell:integration"]) { - const shellState = rtInfo["shell:state"] as ShellIntegrationStatus; + shellState = rtInfo["shell:state"] as ShellIntegrationStatus; globalStore.set(this.shellIntegrationStatusAtom, shellState || null); } else { globalStore.set(this.shellIntegrationStatusAtom, null); } const lastCmd = rtInfo ? rtInfo["shell:lastcmd"] : null; + const isCC = shellState === "running-command" && isClaudeCodeCommand(lastCmd); globalStore.set(this.lastCommandAtom, lastCmd || null); + globalStore.set(this.claudeCodeActiveAtom, isCC); } catch (e) { console.log("Error loading runtime info:", e); } @@ -371,7 +401,9 @@ export class TermWrap { this.promptMarkers.forEach((marker) => { try { marker.dispose(); - } catch (_) {} + } catch (_) { + /* nothing */ + } }); this.promptMarkers = []; this.webglContextLossDisposable?.dispose(); @@ -380,7 +412,9 @@ export class TermWrap { this.toDispose.forEach((d) => { try { d.dispose(); - } catch (_) {} + } catch (_) { + /* nothing */ + } }); this.mainFileSubject.release(); } diff --git a/package-lock.json b/package-lock.json index 9258769417..ee28bc811d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5434,32 +5434,32 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react": { - "version": "0.27.16", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", - "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.1.6", - "@floating-ui/utils": "^0.2.10", + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { @@ -5468,12 +5468,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -5481,9 +5481,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@hapi/hoek": { @@ -15182,9 +15182,9 @@ "license": "MIT" }, "node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, "node_modules/emojilib": { @@ -15353,9 +15353,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.39.10", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", - "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", "license": "MIT", "workspaces": [ "docs", @@ -18948,9 +18948,9 @@ } }, "node_modules/katex": { - "version": "0.16.22", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", - "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "version": "0.16.38", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.38.tgz", + "integrity": "sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -29918,13 +29918,13 @@ } }, "node_modules/swr": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", - "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", "license": "MIT", "dependencies": { "dequal": "^2.0.3", - "use-sync-external-store": "^1.4.0" + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -29947,9 +29947,9 @@ } }, "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, "node_modules/tailwind-merge": { @@ -31867,9 +31867,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" From bff2aa688446c348d8e4179b1bc6ab4db268af71 Mon Sep 17 00:00:00 2001 From: "wave-builder[bot]" <181805596+wave-builder[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:01:09 +0000 Subject: [PATCH 08/11] chore: bump package version to 0.14.4-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ed6d6b2c3..3ddfb7d683 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.14.4-beta.0", + "version": "0.14.4-beta.1", "homepage": "https://waveterm.dev", "build": { "appId": "dev.commandline.waveterm" From 373ced4d4d4ad1a86cb93a88fad4ff3697ac2ed5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:02:17 -0700 Subject: [PATCH 09/11] Bump yaml from 2.8.1 to 2.8.3 (#3124) Bumps [yaml](https://github.com/eemeli/yaml) from 2.8.1 to 2.8.3.
Release notes

Sourced from yaml's releases.

v2.8.3

  • Add trailingComma ToString option for multiline flow formatting (#670)
  • Catch stack overflow during node composition (1e84ebb)

v2.8.2

  • Serialize -0 as -0 (#638)
  • Do not double newlines for empty map values (#642)
Commits
  • ce14587 2.8.3
  • 1e84ebb fix: Catch stack overflow during node composition
  • 6b24090 ci: Include Prettier check in lint action
  • 9424dee chore: Refresh lockfile
  • d1aca82 Add trailingComma ToString option for multiline flow formatting (#670)
  • 4321509 ci: Drop the branch filter from GitHub PR actions
  • 47207d0 chore: Update docs-slate
  • 5212fae chore: Update docs-slate
  • 086fa6b 2.8.2
  • 95f01e9 chore: Add funding to package.json
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=yaml&package-manager=npm_and_yarn&previous-version=2.8.1&new-version=2.8.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/wavetermdev/waveterm/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 15 +++++++++------ package.json | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee28bc811d..00bae215eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.14.4-beta.0", + "version": "0.14.4-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.14.4-beta.0", + "version": "0.14.4-beta.1", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ @@ -84,7 +84,7 @@ "uuid": "^13.0.0", "winston": "^3.19.0", "ws": "^8.19.0", - "yaml": "^2.7.1" + "yaml": "^2.8.3" }, "devDependencies": { "@eslint/js": "^9.39", @@ -33310,15 +33310,18 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 3ddfb7d683..fcfa4e82ea 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "uuid": "^13.0.0", "winston": "^3.19.0", "ws": "^8.19.0", - "yaml": "^2.7.1" + "yaml": "^2.8.3" }, "packageManager": "npm@10.9.2", "workspaces": [ From b436aaecc452dd009e94d52972b7c7548cc6cb9c Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Thu, 26 Mar 2026 15:35:55 -0700 Subject: [PATCH 10/11] fix #3011 (missing blockfile) when splitting durable sessions (#3125) --- pkg/blockcontroller/durableshellcontroller.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/blockcontroller/durableshellcontroller.go b/pkg/blockcontroller/durableshellcontroller.go index d3481b172d..a21dac153b 100644 --- a/pkg/blockcontroller/durableshellcontroller.go +++ b/pkg/blockcontroller/durableshellcontroller.go @@ -7,11 +7,13 @@ import ( "context" "encoding/base64" "fmt" + "io/fs" "log" "sync" "time" "github.com/google/uuid" + "github.com/wavetermdev/waveterm/pkg/filestore" "github.com/wavetermdev/waveterm/pkg/jobcontroller" "github.com/wavetermdev/waveterm/pkg/remote" "github.com/wavetermdev/waveterm/pkg/remote/conncontroller" @@ -163,6 +165,10 @@ func (dsc *DurableShellController) Start(ctx context.Context, blockMeta waveobj. if jobId == "" { log.Printf("block %q starting new durable shell\n", dsc.BlockId) + fsErr := filestore.WFS.MakeFile(ctx, dsc.BlockId, wavebase.BlockFile_Term, nil, wshrpc.FileOpts{MaxSize: DefaultTermMaxFileSize, Circular: true}) + if fsErr != nil && fsErr != fs.ErrExist { + return fmt.Errorf("error creating block term file: %w", fsErr) + } newJobId, err := dsc.startNewJob(ctx, blockMeta, dsc.ConnName, rtOpts) if err != nil { return fmt.Errorf("failed to start new job: %w", err) From 2743d71b87a6424b7765592e17779d5205db68ac Mon Sep 17 00:00:00 2001 From: "wave-builder[bot]" <181805596+wave-builder[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:37:26 +0000 Subject: [PATCH 11/11] chore: bump package version to 0.14.4-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fcfa4e82ea..2b68fef8a7 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.14.4-beta.1", + "version": "0.14.4-beta.2", "homepage": "https://waveterm.dev", "build": { "appId": "dev.commandline.waveterm"