From f76712cf2a62e805fd96113964ed13ebf5234cc0 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Thu, 4 Dec 2025 23:07:38 -0800 Subject: [PATCH 01/32] Add AI-powered automatic tab title generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements LLM-based tab title generation that automatically creates meaningful, short tab titles based on terminal activity. Features: - Analyzes last 4KB of terminal output using configured AI backend - Auto-triggers after 10 lines of terminal output - Respects 14-character UI limit with optimized prompt - 5-minute cooldown to prevent excessive regeneration - Only auto-generates for default tab names (T1, T2, etc.) - Works with Vertex AI Anthropic, OpenAI, and other backends Example titles: npm install → NPM Setup, git commit → Git Commit New files: - pkg/waveai/tabtitle.go: Core title generation logic - pkg/blockcontroller/tabtitle_trigger.go: Automatic trigger system 🤖 Generated with Claude Code Co-Authored-By: Claude --- pkg/blockcontroller/blockcontroller.go | 6 + pkg/blockcontroller/tabtitle_trigger.go | 130 ++++++++++++++++++++ pkg/waveai/tabtitle.go | 156 ++++++++++++++++++++++++ pkg/wshrpc/wshrpctypes.go | 4 + pkg/wshrpc/wshserver/wshserver.go | 8 ++ 5 files changed, 304 insertions(+) create mode 100644 pkg/blockcontroller/tabtitle_trigger.go create mode 100644 pkg/waveai/tabtitle.go diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index a255c680a..ae73fed92 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -319,6 +319,12 @@ func HandleAppendBlockFile(blockId string, blockFile string, data []byte) error Data64: base64.StdEncoding.EncodeToString(data), }, }) + + // Check if we should auto-generate a tab title + if blockFile == wavebase.BlockFile_Term { + CheckAndGenerateTitle(blockId, data) + } + return nil } diff --git a/pkg/blockcontroller/tabtitle_trigger.go b/pkg/blockcontroller/tabtitle_trigger.go new file mode 100644 index 000000000..a1f350417 --- /dev/null +++ b/pkg/blockcontroller/tabtitle_trigger.go @@ -0,0 +1,130 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package blockcontroller + +import ( + "bytes" + "context" + "log" + "sync" + "time" + + "github.com/wavetermdev/waveterm/pkg/waveai" + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wstore" +) + +const ( + LinesThresholdForTitle = 10 // Generate title after N lines of output + TitleCooldownSeconds = 300 // Don't regenerate title more often than every 5 minutes +) + +// tabTitleTracker tracks line counts per tab for auto-generating titles +type tabTitleTracker struct { + mu sync.Mutex + tabLineCounts map[string]int // tabId -> line count + lastTitleGenTime map[string]time.Time // tabId -> last time title was generated +} + +var titleTracker = &tabTitleTracker{ + tabLineCounts: make(map[string]int), + lastTitleGenTime: make(map[string]time.Time), +} + +// CheckAndGenerateTitle checks if we should generate a title for the tab containing this block +func CheckAndGenerateTitle(blockId string, data []byte) { + // Count newlines in the data + newlines := bytes.Count(data, []byte("\n")) + if newlines == 0 { + return // No new lines, nothing to do + } + + // Get the tab that contains this block + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + block, err := wstore.DBGet[*waveobj.Block](ctx, blockId) + if err != nil || block == nil { + return + } + + // Extract tabId from parent ORef (format: "tab:uuid") + if block.ParentORef == "" { + return + } + oref, err := waveobj.ParseORef(block.ParentORef) + if err != nil || oref.OType != waveobj.OType_Tab { + return + } + tabId := oref.OID + + // Update line count and check threshold + titleTracker.mu.Lock() + titleTracker.tabLineCounts[tabId] += newlines + lineCount := titleTracker.tabLineCounts[tabId] + lastGenTime, exists := titleTracker.lastTitleGenTime[tabId] + titleTracker.mu.Unlock() + + // Check if we've hit the threshold + if lineCount < LinesThresholdForTitle { + return + } + + // Check cooldown period + if exists && time.Since(lastGenTime).Seconds() < TitleCooldownSeconds { + return + } + + // Check if tab already has a custom name (don't override user-set names) + tab, err := wstore.DBGet[*waveobj.Tab](ctx, tabId) + if err != nil || tab == nil { + return + } + + // Only auto-generate for default names like "T1", "T2", etc. + if !isDefaultTabName(tab.Name) { + return + } + + // Reset counter and update last gen time before generating (to prevent duplicates) + titleTracker.mu.Lock() + titleTracker.tabLineCounts[tabId] = 0 + titleTracker.lastTitleGenTime[tabId] = time.Now() + titleTracker.mu.Unlock() + + // Generate title asynchronously + go func() { + genCtx, genCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer genCancel() + + title, err := waveai.GenerateTabTitle(genCtx, tabId) + if err != nil { + log.Printf("Error generating tab title for tab %s: %v", tabId, err) + return + } + + // Update the tab name + err = wstore.UpdateTabName(genCtx, tabId, title) + if err != nil { + log.Printf("Error updating tab name for tab %s: %v", tabId, err) + return + } + + log.Printf("Auto-generated tab title for tab %s: %q", tabId, title) + }() +} + +// isDefaultTabName checks if a tab name is the default pattern (T1, T2, etc.) +func isDefaultTabName(name string) bool { + if len(name) < 2 || name[0] != 'T' { + return false + } + // Check if the rest is a number + for i := 1; i < len(name); i++ { + if name[i] < '0' || name[i] > '9' { + return false + } + } + return true +} diff --git a/pkg/waveai/tabtitle.go b/pkg/waveai/tabtitle.go new file mode 100644 index 000000000..70efeb7d5 --- /dev/null +++ b/pkg/waveai/tabtitle.go @@ -0,0 +1,156 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package waveai + +import ( + "context" + "fmt" + "io/fs" + "log" + "strings" + + "github.com/wavetermdev/waveterm/pkg/filestore" + "github.com/wavetermdev/waveterm/pkg/wavebase" + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wconfig" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wstore" +) + +const MaxTerminalContentForTitle = 4000 // Maximum bytes of terminal content to analyze + +// GenerateTabTitle uses AI to generate a short, meaningful title for a tab based on its terminal content +func GenerateTabTitle(ctx context.Context, tabId string) (string, error) { + // Get the tab + tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) + if err != nil { + return "", fmt.Errorf("error getting tab: %w", err) + } + + // If no blocks, return default + if len(tab.BlockIds) == 0 { + return "", fmt.Errorf("tab has no blocks") + } + + // Get terminal content from the first block (usually the primary terminal) + blockId := tab.BlockIds[0] + terminalContent, err := getTerminalContent(ctx, blockId, MaxTerminalContentForTitle) + if err != nil { + return "", fmt.Errorf("error getting terminal content: %w", err) + } + + if terminalContent == "" { + return "", fmt.Errorf("no terminal content available") + } + + // Generate title using AI + title, err := generateTitleFromContent(ctx, terminalContent) + if err != nil { + return "", fmt.Errorf("error generating title: %w", err) + } + + return title, nil +} + +// getTerminalContent reads the last N bytes of terminal output from a block +func getTerminalContent(ctx context.Context, blockId string, maxBytes int) (string, error) { + // Read the terminal file + _, data, err := filestore.WFS.ReadFile(ctx, blockId, wavebase.BlockFile_Term) + if err != nil { + if err == fs.ErrNotExist { + return "", nil + } + return "", fmt.Errorf("error reading terminal file: %w", err) + } + + // If data is larger than maxBytes, take the last maxBytes + if len(data) > maxBytes { + data = data[len(data)-maxBytes:] + } + + return string(data), nil +} + +// generateTitleFromContent uses AI to generate a short title from terminal content +func generateTitleFromContent(ctx context.Context, content string) (string, error) { + // Get AI settings + fullConfig := wconfig.GetWatcher().GetFullConfig() + aiSettings := fullConfig.Settings.GetAiSettings() + + // Build AI options + aiOpts := &wshrpc.WaveAIOptsType{ + APIType: aiSettings.AiApiType, + BaseURL: aiSettings.AiBaseURL, + Model: aiSettings.AiModel, + } + + // If no API type set, use Vertex AI Anthropic if available, otherwise cloud + if aiOpts.APIType == "" { + if aiOpts.BaseURL != "" { + aiOpts.APIType = APIType_VertexAIAnthropic + } else { + aiOpts.APIType = APIType_OpenAI // Will use cloud backend + } + } + + // Set model if not specified + if aiOpts.Model == "" { + if aiOpts.APIType == APIType_VertexAIAnthropic { + aiOpts.Model = "claude-3-5-haiku@20241022" + } else { + aiOpts.Model = "default" + } + } + + // Prepare the prompt + prompt := fmt.Sprintf(`Based on this terminal output, generate a SHORT tab title (maximum 12 characters, preferably 6-10). +The title should capture the main activity or purpose. Use abbreviations if needed. + +Examples: +- "npm install react" → "NPM Setup" +- "cd ~/projects/myapp && git status" → "Git MyApp" +- "docker ps -a" → "Docker" +- "python train.py" → "Train ML" +- "ssh user@server" → "SSH Srv" + +Terminal output: +%s + +Respond with ONLY the title text, nothing else. Keep it under 12 characters.`, content) + + // Create AI request + request := wshrpc.WaveAIStreamRequest{ + Opts: aiOpts, + Prompt: []wshrpc.WaveAIPromptMessageType{ + { + Role: "user", + Content: prompt, + }, + }, + } + + // Stream the completion + responseChan := RunAICommand(ctx, request) + + // Collect the response + var titleBuilder strings.Builder + for respUnion := range responseChan { + if respUnion.Error != nil { + return "", fmt.Errorf("AI error: %w", respUnion.Error) + } + if respUnion.Response.Text != "" { + titleBuilder.WriteString(respUnion.Response.Text) + } + } + + title := strings.TrimSpace(titleBuilder.String()) + + // Ensure it's not too long (strict 14 char limit from UI) + if len(title) > 14 { + title = title[:14] + } + + log.Printf("Generated tab title: %q", title) + return title, nil +} diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 0191a4e4e..54ec5f3df 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -186,6 +186,9 @@ const ( Command_GetSecretsNames = "getsecretsnames" Command_SetSecrets = "setsecrets" Command_GetSecretsLinuxStorageBackend = "getsecretslinuxstoragebackend" + + // tab + Command_GenerateTabTitle = "generatetabtitle" ) type RespOrErrorUnion[T any] struct { @@ -261,6 +264,7 @@ type WshRpcInterface interface { FetchSuggestionsCommand(ctx context.Context, data FetchSuggestionsData) (*FetchSuggestionsResponse, error) DisposeSuggestionsCommand(ctx context.Context, widgetId string) error GetTabCommand(ctx context.Context, tabId string) (*waveobj.Tab, error) + GenerateTabTitleCommand(ctx context.Context, tabId string) (string, error) // connection functions ConnStatusCommand(ctx context.Context) ([]ConnStatus, error) diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index f211d71e7..fe7237d20 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -1408,6 +1408,14 @@ func (ws *WshServer) GetTabCommand(ctx context.Context, tabId string) (*waveobj. return tab, nil } +func (ws *WshServer) GenerateTabTitleCommand(ctx context.Context, tabId string) (string, error) { + title, err := waveai.GenerateTabTitle(ctx, tabId) + if err != nil { + return "", fmt.Errorf("error generating tab title: %w", err) + } + return title, nil +} + func (ws *WshServer) GetSecretsCommand(ctx context.Context, names []string) (map[string]string, error) { result := make(map[string]string) for _, name := range names { From e08c0cb5f8b7137409a70703384fdf2e299bc26e Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Thu, 4 Dec 2025 23:07:48 -0800 Subject: [PATCH 02/32] Add drag-and-drop file support to terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements drag-and-drop functionality for terminal, matching macOS Terminal behavior. Users can now drag files or folders from Finder directly into the terminal to insert their paths. Features: - Automatically inserts file paths when files are dropped - Handles multiple files (space-separated) - Auto-quotes paths containing spaces - Uses full file paths from Electron File API - Works with files, folders, and multiple selections Usage: Simply drag a file from Finder and drop it into any terminal block. The file path will be inserted at the cursor position. 🤖 Generated with Claude Code Co-Authored-By: Claude --- frontend/app/view/term/term.tsx | 63 ++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 387155752..1ac15a6e7 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -349,8 +349,69 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => const termBg = computeBgStyleFromMeta(blockData?.meta); + // Handle drag and drop + const handleDragOver = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + // Indicate that we can accept the drop + e.dataTransfer.dropEffect = "copy"; + }, []); + + const handleDrop = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Get files from the drop + const files = Array.from(e.dataTransfer.files); + if (files.length === 0) { + return; + } + + // Get the file path(s) - for Electron, we can get the full path + const paths = files.map((file: any) => { + // In Electron, File objects have a 'path' property with the full path + if (file.path) { + return file.path; + } + // Fallback to just the name if path is not available + return file.name; + }); + + // Insert the path(s) into the terminal + // If multiple files, separate with spaces and quote if necessary + const pathString = paths.map(path => { + // Quote paths that contain spaces + if (path.includes(" ")) { + return `"${path}"`; + } + return path; + }).join(" "); + + // Send the path to the terminal + if (model.termRef.current && pathString) { + model.sendDataToController(pathString); + } + }, [model]); + + const handleDragEnter = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDragLeave = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + return ( -
+
{termBg &&
} From 85858e12f2b0331bf7700b5f5831826ccb7f4014 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Thu, 4 Dec 2025 23:11:41 -0800 Subject: [PATCH 03/32] Fix drag-and-drop to use full file paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Electron's webUtils.getPathForFile() API to get the actual full file path instead of just the file name. This matches macOS Terminal behavior where dragging a file inserts its complete path. Before: CV_Document.pdf After: /Users/steven/Downloads/CV_Document.pdf 🤖 Generated with Claude Code Co-Authored-By: Claude --- frontend/app/view/term/term.tsx | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 1ac15a6e7..cb7c16df3 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -357,7 +357,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => e.dataTransfer.dropEffect = "copy"; }, []); - const handleDrop = React.useCallback((e: React.DragEvent) => { + const handleDrop = React.useCallback(async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -367,15 +367,24 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => return; } - // Get the file path(s) - for Electron, we can get the full path - const paths = files.map((file: any) => { - // In Electron, File objects have a 'path' property with the full path - if (file.path) { - return file.path; - } - // Fallback to just the name if path is not available - return file.name; - }); + // Get the file path(s) - Use Electron's webUtils to get real paths + let paths: string[] = []; + try { + // In Electron, we need to use webUtils.getPathForFile to get the actual path + const { webUtils } = await import("electron"); + paths = files.map((file: File) => { + try { + return webUtils.getPathForFile(file); + } catch (err) { + console.warn("Could not get path for file:", file.name, err); + return file.name; + } + }); + } catch (err) { + // If webUtils is not available (non-Electron environment), fallback to file.name + console.warn("webUtils not available, using file names only"); + paths = files.map(f => f.name); + } // Insert the path(s) into the terminal // If multiple files, separate with spaces and quote if necessary From da2595c937b4c1c444ca7ad5e42526de7ced3b49 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Thu, 4 Dec 2025 23:15:10 -0800 Subject: [PATCH 04/32] Add Electron API for getting file paths in drag-drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose webUtils.getPathForFile through the Electron preload API so the renderer process can get full file system paths from File objects during drag-and-drop operations. Changes: - Added getPathForFile method to preload script using webUtils - Updated ElectronApi TypeScript interface - Simplified terminal drag-drop to use new API via getApi() This properly implements macOS Terminal-style drag-and-drop with full file paths instead of just filenames. 🤖 Generated with Claude Code Co-Authored-By: Claude --- emain/preload.ts | 3 ++- frontend/app/view/term/term.tsx | 30 +++++++++++------------------- frontend/types/custom.d.ts | 1 + 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/emain/preload.ts b/emain/preload.ts index c6bdf1498..ee60c1360 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -1,7 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { contextBridge, ipcRenderer, Rectangle, WebviewTag } from "electron"; +import { contextBridge, ipcRenderer, Rectangle, webUtils, WebviewTag } from "electron"; // update type in custom.d.ts (ElectronApi type) contextBridge.exposeInMainWorld("api", { @@ -68,6 +68,7 @@ contextBridge.exposeInMainWorld("api", { openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId), setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId), doRefresh: () => ipcRenderer.send("do-refresh"), + getPathForFile: (file: File) => webUtils.getPathForFile(file), }); // Custom event for "new-window" diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index cb7c16df3..f2360fdca 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -357,7 +357,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => e.dataTransfer.dropEffect = "copy"; }, []); - const handleDrop = React.useCallback(async (e: React.DragEvent) => { + const handleDrop = React.useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -367,24 +367,16 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => return; } - // Get the file path(s) - Use Electron's webUtils to get real paths - let paths: string[] = []; - try { - // In Electron, we need to use webUtils.getPathForFile to get the actual path - const { webUtils } = await import("electron"); - paths = files.map((file: File) => { - try { - return webUtils.getPathForFile(file); - } catch (err) { - console.warn("Could not get path for file:", file.name, err); - return file.name; - } - }); - } catch (err) { - // If webUtils is not available (non-Electron environment), fallback to file.name - console.warn("webUtils not available, using file names only"); - paths = files.map(f => f.name); - } + // Get the file path(s) using the Electron API + const paths = files.map((file: File) => { + try { + // Use the exposed Electron API to get the full path + return getApi().getPathForFile(file); + } catch (err) { + console.warn("Could not get path for file:", file.name, err); + return file.name; + } + }); // Insert the path(s) into the terminal // If multiple files, separate with spaces and quote if necessary diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 3476fe539..b43f31c8c 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -130,6 +130,7 @@ declare global { openBuilder: (appId?: string) => void; // open-builder setBuilderWindowAppId: (appId: string) => void; // set-builder-window-appid doRefresh: () => void; // do-refresh + getPathForFile: (file: File) => string; // get-path-for-file }; type ElectronContextMenuItem = { From 7b73a2db7c336c9baacb7d07666e885403ef8af5 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Thu, 4 Dec 2025 23:19:51 -0800 Subject: [PATCH 05/32] Fix: Import getApi in terminal component for drag-drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added missing import of getApi from @/store/global to fix 'getApi is not defined' error when dropping files. Also added debug logging to help troubleshoot file path retrieval. 🤖 Generated with Claude Code Co-Authored-By: Claude --- frontend/app/view/term/term.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index f2360fdca..8104cc7f7 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -7,7 +7,7 @@ import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import type { TermViewModel } from "@/app/view/term/term-model"; -import { atoms, getOverrideConfigAtom, getSettingsPrefixAtom, globalStore, WOS } from "@/store/global"; +import { atoms, getApi, getOverrideConfigAtom, getSettingsPrefixAtom, globalStore, WOS } from "@/store/global"; import { fireAndForget, useAtomValueSafe } from "@/util/util"; import { computeBgStyleFromMeta } from "@/util/waveutil"; import { ISearchOptions } from "@xterm/addon-search"; @@ -367,17 +367,23 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => return; } + console.log("Drop files:", files); + // Get the file path(s) using the Electron API const paths = files.map((file: File) => { try { // Use the exposed Electron API to get the full path - return getApi().getPathForFile(file); + const fullPath = getApi().getPathForFile(file); + console.log("File:", file.name, "-> Full path:", fullPath); + return fullPath; } catch (err) { - console.warn("Could not get path for file:", file.name, err); + console.error("Could not get path for file:", file.name, err); return file.name; } }); + console.log("Paths to insert:", paths); + // Insert the path(s) into the terminal // If multiple files, separate with spaces and quote if necessary const pathString = paths.map(path => { @@ -388,6 +394,8 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => return path; }).join(" "); + console.log("Final path string:", pathString); + // Send the path to the terminal if (model.termRef.current && pathString) { model.sendDataToController(pathString); From da20c8729e166454fcabc2233f3b4e91462c63a9 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Fri, 5 Dec 2025 23:38:54 -0800 Subject: [PATCH 06/32] Replace AI tab titles with folder-based naming and UX improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace AI-powered tab title generation with simple folder name from cwd - Show last folder name from current working directory as tab title - Reduce auto-title trigger threshold from 10 to 1 line for immediate updates - Remove default-name-only restriction (updates any tab name) - Reduce cooldown from 5 minutes to 10 seconds for faster updates - Add manual refresh button to all file preview types (code, markdown, PDFs, images) - Inherit working directory when creating new tabs (Cmd+T preserves cwd) - Fix terminal paste scroll position jumping to top 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/preview/preview-edit.tsx | 4 + .../app/view/preview/preview-markdown.tsx | 12 +- frontend/app/view/preview/preview-model.tsx | 17 +++ .../app/view/preview/preview-streaming.tsx | 10 ++ frontend/app/view/term/termwrap.ts | 12 ++ pkg/blockcontroller/tabtitle_trigger.go | 35 +---- pkg/waveai/tabtitle.go | 140 ++++-------------- pkg/wcore/workspace.go | 42 +++++- 8 files changed, 121 insertions(+), 151 deletions(-) diff --git a/frontend/app/view/preview/preview-edit.tsx b/frontend/app/view/preview/preview-edit.tsx index 3e79dbced..f2df68ba6 100644 --- a/frontend/app/view/preview/preview-edit.tsx +++ b/frontend/app/view/preview/preview-edit.tsx @@ -63,9 +63,13 @@ function CodeEditPreview({ model }: SpecializedViewProps) { useEffect(() => { model.codeEditKeyDownHandler = codeEditKeyDownHandler; + model.refreshCallback = () => { + globalStore.set(model.refreshVersion, (v) => v + 1); + }; return () => { model.codeEditKeyDownHandler = null; model.monacoRef.current = null; + model.refreshCallback = null; }; }, []); diff --git a/frontend/app/view/preview/preview-markdown.tsx b/frontend/app/view/preview/preview-markdown.tsx index 6eda00d3c..22bba8888 100644 --- a/frontend/app/view/preview/preview-markdown.tsx +++ b/frontend/app/view/preview/preview-markdown.tsx @@ -2,12 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 import { Markdown } from "@/element/markdown"; -import { getOverrideConfigAtom } from "@/store/global"; +import { getOverrideConfigAtom, globalStore } from "@/store/global"; import { useAtomValue } from "jotai"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import type { SpecializedViewProps } from "./preview"; function MarkdownPreview({ model }: SpecializedViewProps) { + useEffect(() => { + model.refreshCallback = () => { + globalStore.set(model.refreshVersion, (v) => v + 1); + }; + return () => { + model.refreshCallback = null; + }; + }, []); const connName = useAtomValue(model.connection); const fileInfo = useAtomValue(model.statFile); const fontSizeOverride = useAtomValue(getOverrideConfigAtom(model.blockId, "markdown:fontsize")); diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index b6aa8313b..27482550c 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -350,6 +350,22 @@ export class PreviewModel implements ViewModel { title: "Table of Contents", click: () => this.markdownShowTocToggle(), }, + { + elemtype: "iconbutton", + icon: "arrows-rotate", + title: "Refresh", + click: () => this.refreshCallback?.(), + }, + ] as IconButtonDecl[]; + } else if (!isCeView && mimeType) { + // For all other file types (text, code, etc.), add refresh button + return [ + { + elemtype: "iconbutton", + icon: "arrows-rotate", + title: "Refresh", + click: () => this.refreshCallback?.(), + }, ] as IconButtonDecl[]; } return null; @@ -408,6 +424,7 @@ export class PreviewModel implements ViewModel { this.goParentDirectory = this.goParentDirectory.bind(this); const fullFileAtom = atom>(async (get) => { + get(this.refreshVersion); // Subscribe to refreshVersion to trigger re-fetch const fileName = get(this.metaFilePath); const path = await this.formatRemoteUri(fileName, get); if (fileName == null) { diff --git a/frontend/app/view/preview/preview-streaming.tsx b/frontend/app/view/preview/preview-streaming.tsx index f16babe7f..d45a362c4 100644 --- a/frontend/app/view/preview/preview-streaming.tsx +++ b/frontend/app/view/preview/preview-streaming.tsx @@ -3,9 +3,11 @@ import { Button } from "@/app/element/button"; import { CenteredDiv } from "@/app/element/quickelems"; +import { globalStore } from "@/store/global"; import { getWebServerEndpoint } from "@/util/endpoints"; import { formatRemoteUri } from "@/util/waveutil"; import { useAtomValue } from "jotai"; +import { useEffect } from "react"; import { TransformComponent, TransformWrapper, useControls } from "react-zoom-pan-pinch"; import type { SpecializedViewProps } from "./preview"; @@ -45,6 +47,14 @@ function StreamingImagePreview({ url }: { url: string }) { } function StreamingPreview({ model }: SpecializedViewProps) { + useEffect(() => { + model.refreshCallback = () => { + globalStore.set(model.refreshVersion, (v) => v + 1); + }; + return () => { + model.refreshCallback = null; + }; + }, []); const conn = useAtomValue(model.connection); const fileInfo = useAtomValue(model.statFile); const filePath = fileInfo.path; diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 8ecaef08c..f7fb197e2 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -745,6 +745,9 @@ export class TermWrap { e?.preventDefault(); e?.stopPropagation(); + // Preserve scroll position before paste + const scrollY = this.terminal.buffer.active.viewportY; + try { const clipboardData = await extractAllClipboardData(e); let firstImage = true; @@ -761,6 +764,15 @@ export class TermWrap { this.terminal.paste(data.text); } } + + // Restore scroll position after paste if it changed unexpectedly + setTimeout(() => { + const currentScrollY = this.terminal.buffer.active.viewportY; + // Only restore if we've scrolled significantly (not just normal paste scroll) + if (currentScrollY < scrollY - 10) { + this.terminal.scrollToLine(scrollY); + } + }, 50); } catch (err) { console.error("Paste error:", err); } finally { diff --git a/pkg/blockcontroller/tabtitle_trigger.go b/pkg/blockcontroller/tabtitle_trigger.go index a1f350417..b6e2b6321 100644 --- a/pkg/blockcontroller/tabtitle_trigger.go +++ b/pkg/blockcontroller/tabtitle_trigger.go @@ -16,15 +16,15 @@ import ( ) const ( - LinesThresholdForTitle = 10 // Generate title after N lines of output - TitleCooldownSeconds = 300 // Don't regenerate title more often than every 5 minutes + LinesThresholdForTitle = 1 // Generate title after N lines of output + TitleCooldownSeconds = 10 // Don't regenerate title more often than every 10 seconds ) // tabTitleTracker tracks line counts per tab for auto-generating titles type tabTitleTracker struct { - mu sync.Mutex - tabLineCounts map[string]int // tabId -> line count - lastTitleGenTime map[string]time.Time // tabId -> last time title was generated + mu sync.Mutex + tabLineCounts map[string]int // tabId -> line count + lastTitleGenTime map[string]time.Time // tabId -> last time title was generated } var titleTracker = &tabTitleTracker{ @@ -76,17 +76,6 @@ func CheckAndGenerateTitle(blockId string, data []byte) { return } - // Check if tab already has a custom name (don't override user-set names) - tab, err := wstore.DBGet[*waveobj.Tab](ctx, tabId) - if err != nil || tab == nil { - return - } - - // Only auto-generate for default names like "T1", "T2", etc. - if !isDefaultTabName(tab.Name) { - return - } - // Reset counter and update last gen time before generating (to prevent duplicates) titleTracker.mu.Lock() titleTracker.tabLineCounts[tabId] = 0 @@ -114,17 +103,3 @@ func CheckAndGenerateTitle(blockId string, data []byte) { log.Printf("Auto-generated tab title for tab %s: %q", tabId, title) }() } - -// isDefaultTabName checks if a tab name is the default pattern (T1, T2, etc.) -func isDefaultTabName(name string) bool { - if len(name) < 2 || name[0] != 'T' { - return false - } - // Check if the rest is a number - for i := 1; i < len(name); i++ { - if name[i] < '0' || name[i] > '9' { - return false - } - } - return true -} diff --git a/pkg/waveai/tabtitle.go b/pkg/waveai/tabtitle.go index 70efeb7d5..9703ec164 100644 --- a/pkg/waveai/tabtitle.go +++ b/pkg/waveai/tabtitle.go @@ -6,21 +6,14 @@ package waveai import ( "context" "fmt" - "io/fs" - "log" + "path/filepath" "strings" - "github.com/wavetermdev/waveterm/pkg/filestore" - "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/waveobj" - "github.com/wavetermdev/waveterm/pkg/wconfig" - "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wstore" ) -const MaxTerminalContentForTitle = 4000 // Maximum bytes of terminal content to analyze - -// GenerateTabTitle uses AI to generate a short, meaningful title for a tab based on its terminal content +// GenerateTabTitle generates a short title for a tab based on the current working directory func GenerateTabTitle(ctx context.Context, tabId string) (string, error) { // Get the tab tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) @@ -33,124 +26,45 @@ func GenerateTabTitle(ctx context.Context, tabId string) (string, error) { return "", fmt.Errorf("tab has no blocks") } - // Get terminal content from the first block (usually the primary terminal) + // Get the first block (usually the primary terminal) blockId := tab.BlockIds[0] - terminalContent, err := getTerminalContent(ctx, blockId, MaxTerminalContentForTitle) + block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId) if err != nil { - return "", fmt.Errorf("error getting terminal content: %w", err) - } - - if terminalContent == "" { - return "", fmt.Errorf("no terminal content available") + return "", fmt.Errorf("error getting block: %w", err) } - // Generate title using AI - title, err := generateTitleFromContent(ctx, terminalContent) - if err != nil { - return "", fmt.Errorf("error generating title: %w", err) + // Get the current working directory from block metadata + meta := waveobj.GetMeta(block) + cwd, ok := meta[waveobj.MetaKey_CmdCwd].(string) + if !ok || cwd == "" { + return "", fmt.Errorf("no working directory available") } + // Generate title from the last 2 folders + title := generateTitleFromPath(cwd) return title, nil } -// getTerminalContent reads the last N bytes of terminal output from a block -func getTerminalContent(ctx context.Context, blockId string, maxBytes int) (string, error) { - // Read the terminal file - _, data, err := filestore.WFS.ReadFile(ctx, blockId, wavebase.BlockFile_Term) - if err != nil { - if err == fs.ErrNotExist { - return "", nil - } - return "", fmt.Errorf("error reading terminal file: %w", err) - } +// generateTitleFromPath creates a title from the last folder in a path +func generateTitleFromPath(fullPath string) string { + // Clean the path (remove trailing slashes, etc.) + cleanPath := filepath.Clean(fullPath) - // If data is larger than maxBytes, take the last maxBytes - if len(data) > maxBytes { - data = data[len(data)-maxBytes:] - } + // Split the path into components + parts := strings.Split(cleanPath, string(filepath.Separator)) - return string(data), nil -} - -// generateTitleFromContent uses AI to generate a short title from terminal content -func generateTitleFromContent(ctx context.Context, content string) (string, error) { - // Get AI settings - fullConfig := wconfig.GetWatcher().GetFullConfig() - aiSettings := fullConfig.Settings.GetAiSettings() - - // Build AI options - aiOpts := &wshrpc.WaveAIOptsType{ - APIType: aiSettings.AiApiType, - BaseURL: aiSettings.AiBaseURL, - Model: aiSettings.AiModel, - } - - // If no API type set, use Vertex AI Anthropic if available, otherwise cloud - if aiOpts.APIType == "" { - if aiOpts.BaseURL != "" { - aiOpts.APIType = APIType_VertexAIAnthropic - } else { - aiOpts.APIType = APIType_OpenAI // Will use cloud backend + // Filter out empty parts + var nonEmptyParts []string + for _, part := range parts { + if part != "" { + nonEmptyParts = append(nonEmptyParts, part) } } - // Set model if not specified - if aiOpts.Model == "" { - if aiOpts.APIType == APIType_VertexAIAnthropic { - aiOpts.Model = "claude-3-5-haiku@20241022" - } else { - aiOpts.Model = "default" - } + if len(nonEmptyParts) == 0 { + return "/" } - // Prepare the prompt - prompt := fmt.Sprintf(`Based on this terminal output, generate a SHORT tab title (maximum 12 characters, preferably 6-10). -The title should capture the main activity or purpose. Use abbreviations if needed. - -Examples: -- "npm install react" → "NPM Setup" -- "cd ~/projects/myapp && git status" → "Git MyApp" -- "docker ps -a" → "Docker" -- "python train.py" → "Train ML" -- "ssh user@server" → "SSH Srv" - -Terminal output: -%s - -Respond with ONLY the title text, nothing else. Keep it under 12 characters.`, content) - - // Create AI request - request := wshrpc.WaveAIStreamRequest{ - Opts: aiOpts, - Prompt: []wshrpc.WaveAIPromptMessageType{ - { - Role: "user", - Content: prompt, - }, - }, - } - - // Stream the completion - responseChan := RunAICommand(ctx, request) - - // Collect the response - var titleBuilder strings.Builder - for respUnion := range responseChan { - if respUnion.Error != nil { - return "", fmt.Errorf("AI error: %w", respUnion.Error) - } - if respUnion.Response.Text != "" { - titleBuilder.WriteString(respUnion.Response.Text) - } - } - - title := strings.TrimSpace(titleBuilder.String()) - - // Ensure it's not too long (strict 14 char limit from UI) - if len(title) > 14 { - title = title[:14] - } - - log.Printf("Generated tab title: %q", title) - return title, nil + // Use just the last folder name + return nonEmptyParts[len(nonEmptyParts)-1] } diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index 9037b2e5f..b9eddb151 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -199,16 +199,36 @@ func getTabPresetMeta() (waveobj.MetaMapType, error) { // returns tabid func CreateTab(ctx context.Context, workspaceId string, tabName string, activateTab bool, pinned bool, isInitialLaunch bool) (string, error) { + ws, err := GetWorkspace(ctx, workspaceId) + if err != nil { + return "", fmt.Errorf("workspace %s not found: %w", workspaceId, err) + } + if tabName == "" { - ws, err := GetWorkspace(ctx, workspaceId) - if err != nil { - return "", fmt.Errorf("workspace %s not found: %w", workspaceId, err) - } tabName = "T" + fmt.Sprint(len(ws.TabIds)+len(ws.PinnedTabIds)+1) } + // Try to inherit cwd from the active tab + var inheritedMeta waveobj.MetaMapType + if ws.ActiveTabId != "" && !isInitialLaunch { + activeTab, _ := wstore.DBGet[*waveobj.Tab](ctx, ws.ActiveTabId) + if activeTab != nil && len(activeTab.BlockIds) > 0 { + // Get the first block from the active tab + firstBlock, _ := wstore.DBGet[*waveobj.Block](ctx, activeTab.BlockIds[0]) + if firstBlock != nil { + meta := waveobj.GetMeta(firstBlock) + if cwd, ok := meta[waveobj.MetaKey_CmdCwd].(string); ok && cwd != "" { + // Inherit the cwd for the new tab + inheritedMeta = waveobj.MetaMapType{ + waveobj.MetaKey_CmdCwd: cwd, + } + } + } + } + } + // The initial tab for the initial launch should be pinned - tab, err := createTabObj(ctx, workspaceId, tabName, pinned || isInitialLaunch, nil) + tab, err := createTabObj(ctx, workspaceId, tabName, pinned || isInitialLaunch, inheritedMeta) if err != nil { return "", fmt.Errorf("error creating tab: %w", err) } @@ -221,7 +241,17 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate // No need to apply an initial layout for the initial launch, since the starter layout will get applied after onboarding modal dismissal if !isInitialLaunch { - err = ApplyPortableLayout(ctx, tab.OID, GetNewTabLayout(), true) + newTabLayout := GetNewTabLayout() + // Merge inherited cwd into the terminal block's meta + if len(inheritedMeta) > 0 && len(newTabLayout) > 0 { + if newTabLayout[0].BlockDef.Meta == nil { + newTabLayout[0].BlockDef.Meta = make(waveobj.MetaMapType) + } + for k, v := range inheritedMeta { + newTabLayout[0].BlockDef.Meta[k] = v + } + } + err = ApplyPortableLayout(ctx, tab.OID, newTabLayout, true) if err != nil { return tab.OID, fmt.Errorf("error applying new tab layout: %w", err) } From da450d0c9cffaeaa9d51c9d9695af90e5d8eee24 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sun, 7 Dec 2025 23:36:56 -0800 Subject: [PATCH 07/32] Add UX improvements: scroll preservation, close confirmation, and markdown copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Electron name in menu bar (remove waveterm/electron setName) - Prevent AI panel auto-scroll when user manually scrolls up to read - Add confirmation dialog before closing tabs to prevent accidental closures - Preserve terminal scroll position during resize and window changes - Add markdown formatting to terminal copy (converts bold text to **bold**) - Terminal copy now preserves formatting when pasting into markdown editors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- emain/emain-platform.ts | 11 ++- frontend/app/aipanel/aipanelmessages.tsx | 40 ++++++++++- frontend/app/modals/confirmclosetab.tsx | 39 ++++++++++ frontend/app/modals/modalregistry.tsx | 2 + frontend/app/tab/tabbar.tsx | 7 +- frontend/app/view/term/termwrap.ts | 91 +++++++++++++++++++++++- 6 files changed, 174 insertions(+), 16 deletions(-) create mode 100644 frontend/app/modals/confirmclosetab.tsx diff --git a/emain/emain-platform.ts b/emain/emain-platform.ts index 32320e4eb..bf4f8cbb4 100644 --- a/emain/emain-platform.ts +++ b/emain/emain-platform.ts @@ -10,12 +10,6 @@ import path from "path"; import { WaveDevVarName, WaveDevViteVarName } from "../frontend/util/isdev"; import * as keyutil from "../frontend/util/keyutil"; -// This is a little trick to ensure that Electron puts all its runtime data into a subdirectory to avoid conflicts with our own data. -// On macOS, it will store to ~/Library/Application \Support/waveterm/electron -// On Linux, it will store to ~/.config/waveterm/electron -// On Windows, it will store to %LOCALAPPDATA%/waveterm/electron -app.setName("waveterm/electron"); - const isDev = !app.isPackaged; const isDevVite = isDev && process.env.ELECTRON_RENDERER_URL; console.log(`Running in ${isDev ? "development" : "production"} mode`); @@ -32,7 +26,12 @@ const waveDirName = `${waveDirNamePrefix}${waveDirNameSuffix ? `-${waveDirNameSu const paths = envPaths("waveterm", { suffix: waveDirNameSuffix }); +// Set the proper display name first app.setName(isDev ? "Wave (Dev)" : "Wave"); + +// Note: We previously used app.setName("waveterm/electron") here to organize Electron's runtime data, +// but this caused "Electron" to appear in the macOS menu bar. The envPaths configuration above +// already handles the data directory organization, so the setName trick is not needed. const unamePlatform = process.platform; const unameArch: string = process.arch; keyutil.setKeyUtilPlatform(unamePlatform); diff --git a/frontend/app/aipanel/aipanelmessages.tsx b/frontend/app/aipanel/aipanelmessages.tsx index a0284153d..8e32f2035 100644 --- a/frontend/app/aipanel/aipanelmessages.tsx +++ b/frontend/app/aipanel/aipanelmessages.tsx @@ -20,25 +20,59 @@ export const AIPanelMessages = memo(({ messages, status, onContextMenu }: AIPane const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const prevStatusRef = useRef(status); + const userHasScrolledUp = useRef(false); + const isAutoScrolling = useRef(false); const scrollToBottom = () => { const container = messagesContainerRef.current; if (container) { + isAutoScrolling.current = true; container.scrollTop = container.scrollHeight; container.scrollLeft = 0; + userHasScrolledUp.current = false; + setTimeout(() => { + isAutoScrolling.current = false; + }, 100); } }; + // Detect if user has manually scrolled up + useEffect(() => { + const container = messagesContainerRef.current; + if (!container) return; + + const handleScroll = () => { + // Ignore scroll events triggered by our auto-scroll + if (isAutoScrolling.current) return; + + const { scrollTop, scrollHeight, clientHeight } = container; + const distanceFromBottom = scrollHeight - scrollTop - clientHeight; + + // If user is more than 50px from the bottom, they've scrolled up + if (distanceFromBottom > 50) { + userHasScrolledUp.current = true; + } else { + userHasScrolledUp.current = false; + } + }; + + container.addEventListener("scroll", handleScroll); + return () => container.removeEventListener("scroll", handleScroll); + }, []); + useEffect(() => { model.registerScrollToBottom(scrollToBottom); }, [model]); useEffect(() => { - scrollToBottom(); + // Only auto-scroll if user hasn't manually scrolled up + if (!userHasScrolledUp.current) { + scrollToBottom(); + } }, [messages]); useEffect(() => { - if (isPanelOpen) { + if (isPanelOpen && !userHasScrolledUp.current) { scrollToBottom(); } }, [isPanelOpen]); @@ -47,7 +81,7 @@ export const AIPanelMessages = memo(({ messages, status, onContextMenu }: AIPane const wasStreaming = prevStatusRef.current === "streaming"; const isNowNotStreaming = status !== "streaming"; - if (wasStreaming && isNowNotStreaming) { + if (wasStreaming && isNowNotStreaming && !userHasScrolledUp.current) { requestAnimationFrame(() => { scrollToBottom(); }); diff --git a/frontend/app/modals/confirmclosetab.tsx b/frontend/app/modals/confirmclosetab.tsx new file mode 100644 index 000000000..a431fcb45 --- /dev/null +++ b/frontend/app/modals/confirmclosetab.tsx @@ -0,0 +1,39 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Modal } from "@/app/modals/modal"; +import { deleteLayoutModelForTab } from "@/layout/index"; +import { atoms, getApi, globalStore } from "@/store/global"; +import { modalsModel } from "@/store/modalmodel"; + +interface ConfirmCloseTabModalProps { + tabId: string; +} + +const ConfirmCloseTabModal = ({ tabId }: ConfirmCloseTabModalProps) => { + const handleConfirmClose = () => { + const ws = globalStore.get(atoms.workspace); + getApi().closeTab(ws.oid, tabId); + deleteLayoutModelForTab(tabId); + modalsModel.popModal(); + }; + + const handleCancel = () => { + modalsModel.popModal(); + }; + + return ( + +
+
Close Tab?
+
+ Are you sure you want to close this tab? This action cannot be undone. +
+
+
+ ); +}; + +ConfirmCloseTabModal.displayName = "ConfirmCloseTabModal"; + +export { ConfirmCloseTabModal }; diff --git a/frontend/app/modals/modalregistry.tsx b/frontend/app/modals/modalregistry.tsx index 53fabde06..a19cb4704 100644 --- a/frontend/app/modals/modalregistry.tsx +++ b/frontend/app/modals/modalregistry.tsx @@ -7,6 +7,7 @@ import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade"; import { DeleteFileModal, PublishAppModal, RenameFileModal } from "@/builder/builder-apppanel"; import { SetSecretDialog } from "@/builder/tabs/builder-secrettab"; import { AboutModal } from "./about"; +import { ConfirmCloseTabModal } from "./confirmclosetab"; import { UserInputModal } from "./userinputmodal"; const modalRegistry: { [key: string]: React.ComponentType } = { @@ -15,6 +16,7 @@ const modalRegistry: { [key: string]: React.ComponentType } = { [UserInputModal.displayName || "UserInputModal"]: UserInputModal, [AboutModal.displayName || "AboutModal"]: AboutModal, [MessageModal.displayName || "MessageModal"]: MessageModal, + [ConfirmCloseTabModal.displayName || "ConfirmCloseTabModal"]: ConfirmCloseTabModal, [PublishAppModal.displayName || "PublishAppModal"]: PublishAppModal, [RenameFileModal.displayName || "RenameFileModal"]: RenameFileModal, [DeleteFileModal.displayName || "DeleteFileModal"]: DeleteFileModal, diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 2a479aa63..7831fda1d 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -620,10 +620,9 @@ const TabBar = memo(({ workspace }: TabBarProps) => { const handleCloseTab = (event: React.MouseEvent | null, tabId: string) => { event?.stopPropagation(); - const ws = globalStore.get(atoms.workspace); - getApi().closeTab(ws.oid, tabId); - tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease"); - deleteLayoutModelForTab(tabId); + + // Show confirmation modal before closing + modalsModel.pushModal("ConfirmCloseTabModal", { tabId }); }; const handlePinChange = useCallback( diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index f7fb197e2..f0642e751 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -28,6 +28,61 @@ const TermCacheFileName = "cache:term:full"; const MinDataProcessedForCache = 100 * 1024; export const SupportsImageInput = true; +// Convert terminal selection to markdown preserving formatting +function convertSelectionToMarkdown(terminal: Terminal): string { + const selection = terminal.getSelectionPosition(); + if (!selection) { + return terminal.getSelection(); + } + + let markdown = ""; + const buffer = terminal.buffer.active; + + for (let y = selection.start.y; y <= selection.end.y; y++) { + const line = buffer.getLine(y); + if (!line) continue; + + const startCol = y === selection.start.y ? selection.start.x : 0; + const endCol = y === selection.end.y ? selection.end.x : line.length; + + let lineText = ""; + let isBold = false; + let currentSegment = ""; + + for (let x = startCol; x < endCol; x++) { + const cell = line.getCell(x); + if (!cell) continue; + + const char = cell.getChars(); + // Check if cell has bold attribute (bit 0 of fg color flags) + const cellBold = (cell.getBgColorMode() & 0x01) !== 0 || (cell.getFgColorMode() & 0x01) !== 0; + + // Handle bold transitions + if (cellBold !== isBold) { + if (currentSegment) { + lineText += isBold ? `**${currentSegment}**` : currentSegment; + currentSegment = ""; + } + isBold = cellBold; + } + + currentSegment += char || " "; + } + + // Flush remaining segment + if (currentSegment) { + lineText += isBold ? `**${currentSegment}**` : currentSegment; + } + + markdown += lineText.trimEnd(); + if (y < selection.end.y) { + markdown += "\n"; + } + } + + return markdown; +} + // detect webgl support function detectWebGLSupport(): boolean { try { @@ -481,13 +536,29 @@ export class TermWrap { if (!globalStore.get(copyOnSelectAtom)) { return; } - const selectedText = this.terminal.getSelection(); - if (selectedText.length > 0) { - navigator.clipboard.writeText(selectedText); + const markdownText = convertSelectionToMarkdown(this.terminal); + if (markdownText.length > 0) { + navigator.clipboard.writeText(markdownText); } }) ) ); + + // Intercept copy events to provide markdown formatting + const copyHandler = (e: ClipboardEvent) => { + const selection = this.terminal.getSelection(); + if (selection.length > 0) { + e.preventDefault(); + const markdownText = convertSelectionToMarkdown(this.terminal); + e.clipboardData?.setData("text/plain", markdownText); + } + }; + this.connectElem.addEventListener("copy", copyHandler); + this.toDispose.push({ + dispose: () => { + this.connectElem.removeEventListener("copy", copyHandler); + }, + }); if (this.onSearchResultsDidChange != null) { this.toDispose.push(this.searchAddon.onDidChangeResults(this.onSearchResultsDidChange.bind(this))); } @@ -701,7 +772,21 @@ export class TermWrap { handleResize() { const oldRows = this.terminal.rows; const oldCols = this.terminal.cols; + + // Preserve scroll position before resize + const wasAtBottom = this.terminal.buffer.active.baseY + this.terminal.rows >= this.terminal.buffer.active.length; + const scrollY = this.terminal.buffer.active.viewportY; + this.fitAddon.fit(); + + // Restore scroll position after resize + if (!wasAtBottom && scrollY > 0) { + // If user wasn't at bottom, try to keep them at the same content + setTimeout(() => { + this.terminal.scrollToLine(scrollY); + }, 0); + } + if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) { const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols }; const wsCommand: SetBlockTermSizeWSCommand = { From f6e27d2b1dbecac91f1e5eb62c606d94c04f02ef Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sun, 7 Dec 2025 23:39:18 -0800 Subject: [PATCH 08/32] Make file browser default to terminal's current working directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When opening the file browser from a terminal block, it now starts in the terminal's current working directory instead of always defaulting to the home directory. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/preview/preview-model.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index 27482550c..a9c4d277b 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -371,8 +371,14 @@ export class PreviewModel implements ViewModel { return null; }); this.metaFilePath = atom((get) => { - const file = get(this.blockAtom)?.meta?.file; + const blockData = get(this.blockAtom); + const file = blockData?.meta?.file; if (isBlank(file)) { + // If no file is set, default to the terminal's current working directory + const cwd = blockData?.meta?.[waveobj.MetaKey_CmdCwd]; + if (!isBlank(cwd)) { + return cwd; + } return "~"; } return file; From 0709c3db88a07bfa88b8b41b3f92140cd5d83aca Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sun, 7 Dec 2025 23:43:24 -0800 Subject: [PATCH 09/32] Add light theme and make file browser follow terminal cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add default-light theme (white background, black text) similar to macOS Terminal - Change default theme from default-dark to default-light - Fix file browser to start in terminal's current working directory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/term/termutil.ts | 2 +- pkg/wconfig/defaultconfig/termthemes.json | 28 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend/app/view/term/termutil.ts b/frontend/app/view/term/termutil.ts index 8a7459e6c..ebd27e84b 100644 --- a/frontend/app/view/term/termutil.ts +++ b/frontend/app/view/term/termutil.ts @@ -1,7 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -export const DefaultTermTheme = "default-dark"; +export const DefaultTermTheme = "default-light"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import base64 from "base64-js"; diff --git a/pkg/wconfig/defaultconfig/termthemes.json b/pkg/wconfig/defaultconfig/termthemes.json index d0a667f0a..f39f89eab 100644 --- a/pkg/wconfig/defaultconfig/termthemes.json +++ b/pkg/wconfig/defaultconfig/termthemes.json @@ -1,7 +1,33 @@ { + "default-light": { + "display:name": "Default Light", + "display:order": 1, + "black": "#000000", + "red": "#C23621", + "green": "#25BC24", + "yellow": "#ADAD27", + "blue": "#492EE1", + "magenta": "#D338D3", + "cyan": "#33BBC8", + "white": "#CBCCCD", + "brightBlack": "#818383", + "brightRed": "#FC391F", + "brightGreen": "#31E722", + "brightYellow": "#EAEC23", + "brightBlue": "#5833FF", + "brightMagenta": "#F935F8", + "brightCyan": "#14F0F0", + "brightWhite": "#E9EBEB", + "gray": "#818383", + "cmdtext": "#000000", + "foreground": "#000000", + "selectionBackground": "#B4D5FE", + "background": "#FFFFFF", + "cursor": "#000000" + }, "default-dark": { "display:name": "Default Dark", - "display:order": 1, + "display:order": 2, "black": "#757575", "red": "#cc685c", "green": "#76c266", From ec0439483646a2f74aa563b592fef8983f8b59e8 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sun, 7 Dec 2025 23:48:37 -0800 Subject: [PATCH 10/32] Use GitHub light theme for markdown code syntax highlighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed from github-dark-dimmed to github (light) theme for better readability and GitHub-style code block appearance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/element/markdown.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/element/markdown.scss b/frontend/app/element/markdown.scss index dee02633e..ab78c0606 100644 --- a/frontend/app/element/markdown.scss +++ b/frontend/app/element/markdown.scss @@ -1,7 +1,7 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -@import url("../../../node_modules/highlight.js/scss/github-dark-dimmed.scss"); +@import url("../../../node_modules/highlight.js/scss/github.scss"); .markdown { display: flex; From 06b20385b9af89a96781adae67514ec63ae16cb7 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sun, 7 Dec 2025 23:54:55 -0800 Subject: [PATCH 11/32] Revert "Use GitHub light theme for markdown code syntax highlighting" This reverts commit bf6928d74dbef09a4fd85cf4d5dd786cae7fdf6c. --- frontend/app/element/markdown.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/element/markdown.scss b/frontend/app/element/markdown.scss index ab78c0606..dee02633e 100644 --- a/frontend/app/element/markdown.scss +++ b/frontend/app/element/markdown.scss @@ -1,7 +1,7 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -@import url("../../../node_modules/highlight.js/scss/github.scss"); +@import url("../../../node_modules/highlight.js/scss/github-dark-dimmed.scss"); .markdown { display: flex; From cf1c51156a0635093d56ee51746d96548eeb3ab1 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sun, 7 Dec 2025 23:54:55 -0800 Subject: [PATCH 12/32] Revert "Add light theme and make file browser follow terminal cwd" This reverts commit d6b685a94521b960b204c96f5dd52d02198400ab. --- frontend/app/view/term/termutil.ts | 2 +- pkg/wconfig/defaultconfig/termthemes.json | 28 +---------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/frontend/app/view/term/termutil.ts b/frontend/app/view/term/termutil.ts index ebd27e84b..8a7459e6c 100644 --- a/frontend/app/view/term/termutil.ts +++ b/frontend/app/view/term/termutil.ts @@ -1,7 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -export const DefaultTermTheme = "default-light"; +export const DefaultTermTheme = "default-dark"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import base64 from "base64-js"; diff --git a/pkg/wconfig/defaultconfig/termthemes.json b/pkg/wconfig/defaultconfig/termthemes.json index f39f89eab..d0a667f0a 100644 --- a/pkg/wconfig/defaultconfig/termthemes.json +++ b/pkg/wconfig/defaultconfig/termthemes.json @@ -1,33 +1,7 @@ { - "default-light": { - "display:name": "Default Light", - "display:order": 1, - "black": "#000000", - "red": "#C23621", - "green": "#25BC24", - "yellow": "#ADAD27", - "blue": "#492EE1", - "magenta": "#D338D3", - "cyan": "#33BBC8", - "white": "#CBCCCD", - "brightBlack": "#818383", - "brightRed": "#FC391F", - "brightGreen": "#31E722", - "brightYellow": "#EAEC23", - "brightBlue": "#5833FF", - "brightMagenta": "#F935F8", - "brightCyan": "#14F0F0", - "brightWhite": "#E9EBEB", - "gray": "#818383", - "cmdtext": "#000000", - "foreground": "#000000", - "selectionBackground": "#B4D5FE", - "background": "#FFFFFF", - "cursor": "#000000" - }, "default-dark": { "display:name": "Default Dark", - "display:order": 2, + "display:order": 1, "black": "#757575", "red": "#cc685c", "green": "#76c266", From 048b7a1f820a4b5f953c64bc1f48f7025362adb8 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sun, 7 Dec 2025 23:57:02 -0800 Subject: [PATCH 13/32] Add White background preset option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added bg@white preset for a clean white background (100% opacity). Appears first in the Backgrounds menu after Default. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pkg/wconfig/defaultconfig/presets.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/wconfig/defaultconfig/presets.json b/pkg/wconfig/defaultconfig/presets.json index 3d7cc1ad3..7586ab570 100644 --- a/pkg/wconfig/defaultconfig/presets.json +++ b/pkg/wconfig/defaultconfig/presets.json @@ -4,6 +4,13 @@ "display:order": -1, "bg:*": true }, + "bg@white": { + "display:name": "White", + "display:order": 1.0, + "bg:*": true, + "bg": "white", + "bg:opacity": 1.0 + }, "bg@rainbow": { "display:name": "Rainbow", "display:order": 2.1, From d59fe565212358fbec3255b7fc00279b32ea95d8 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sun, 7 Dec 2025 23:58:45 -0800 Subject: [PATCH 14/32] Add macOS Basic light terminal theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added macOS-style light theme with black text on white background, matching the default macOS Terminal.app appearance. Use this theme together with the White background preset for a clean light terminal. To use: Right-click tab → select a light theme when it's available in UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pkg/wconfig/defaultconfig/termthemes.json | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/wconfig/defaultconfig/termthemes.json b/pkg/wconfig/defaultconfig/termthemes.json index d0a667f0a..929b255fa 100644 --- a/pkg/wconfig/defaultconfig/termthemes.json +++ b/pkg/wconfig/defaultconfig/termthemes.json @@ -1,4 +1,30 @@ { + "macos-basic": { + "display:name": "macOS Basic", + "display:order": 0, + "black": "#000000", + "red": "#C23621", + "green": "#25BC24", + "yellow": "#ADAD27", + "blue": "#492EE1", + "magenta": "#D338D3", + "cyan": "#33BBC8", + "white": "#CBCCCD", + "brightBlack": "#818383", + "brightRed": "#FC391F", + "brightGreen": "#31E722", + "brightYellow": "#EAEC23", + "brightBlue": "#5833FF", + "brightMagenta": "#F935F8", + "brightCyan": "#14F0F0", + "brightWhite": "#E9EBEB", + "gray": "#818383", + "cmdtext": "#000000", + "foreground": "#000000", + "selectionBackground": "#B4D5FE", + "background": "#FFFFFF", + "cursor": "#000000" + }, "default-dark": { "display:name": "Default Dark", "display:order": 1, From e4164c46cfc2ee1d5320c4a038838f21fa71b44a Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:03:38 -0800 Subject: [PATCH 15/32] Change default terminal font to SF Mono 11pt like macOS Terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed default font from Hack to SF Mono (with fallbacks to Menlo, Monaco) - Changed default font size from 12pt to 11pt to match macOS Terminal.app - Font stack: SF Mono, Menlo, Monaco, Courier New, monospace 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/term/term-model.ts | 4 ++-- frontend/app/view/term/term.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index a1b9eb60d..203aecd3b 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -243,9 +243,9 @@ export class TermViewModel implements ViewModel { const connName = blockData?.meta?.connection; const fullConfig = get(atoms.fullConfigAtom); const connFontSize = fullConfig?.connections?.[connName]?.["term:fontsize"]; - const rtnFontSize = blockData?.meta?.["term:fontsize"] ?? connFontSize ?? settingsFontSize ?? 12; + const rtnFontSize = blockData?.meta?.["term:fontsize"] ?? connFontSize ?? settingsFontSize ?? 11; if (typeof rtnFontSize != "number" || isNaN(rtnFontSize) || rtnFontSize < 4 || rtnFontSize > 64) { - return 12; + return 11; } return rtnFontSize; }); diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 8104cc7f7..ed9a6cb65 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -269,7 +269,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => { theme: termTheme, fontSize: termFontSize, - fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "Hack", + fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "SF Mono, Menlo, Monaco, Courier New, monospace", drawBoldTextInBrightColors: false, fontWeight: "normal", fontWeightBold: "bold", From 633940751c3da329944111bfc2d0d9b3a4f801b6 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:09:14 -0800 Subject: [PATCH 16/32] Use terminal theme background instead of transparent overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Disabled transparent background override in terminal - Terminal now uses the theme's own background color - macOS Basic theme will show its white background - Default background preset changed to white - Removes dependency on background presets for basic white terminal 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/term/termutil.ts | 3 ++- pkg/wconfig/defaultconfig/presets.json | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/app/view/term/termutil.ts b/frontend/app/view/term/termutil.ts index 8a7459e6c..79fca1f74 100644 --- a/frontend/app/view/term/termutil.ts +++ b/frontend/app/view/term/termutil.ts @@ -34,7 +34,8 @@ export function computeTheme( } } let bgcolor = themeCopy.background; - themeCopy.background = "#00000000"; + // Don't make background transparent - use the theme's background color + // themeCopy.background = "#00000000"; return [themeCopy, bgcolor]; } diff --git a/pkg/wconfig/defaultconfig/presets.json b/pkg/wconfig/defaultconfig/presets.json index 7586ab570..91c4485d8 100644 --- a/pkg/wconfig/defaultconfig/presets.json +++ b/pkg/wconfig/defaultconfig/presets.json @@ -2,7 +2,9 @@ "bg@default": { "display:name": "Default", "display:order": -1, - "bg:*": true + "bg:*": true, + "bg": "white", + "bg:opacity": 1.0 }, "bg@white": { "display:name": "White", From cde3b7b73cefe68847397a069ae6067417e622d7 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:12:10 -0800 Subject: [PATCH 17/32] Adjust font rendering to match macOS Terminal appearance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed font weight from 'normal'/'bold' to 400/600 - Added line height 1.2 for better spacing - Better matches macOS Terminal's font rendering 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/term/term.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index ed9a6cb65..e7efddb0e 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -271,13 +271,14 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => fontSize: termFontSize, fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "SF Mono, Menlo, Monaco, Courier New, monospace", drawBoldTextInBrightColors: false, - fontWeight: "normal", - fontWeightBold: "bold", + fontWeight: "400", + fontWeightBold: "600", allowTransparency: true, scrollback: termScrollback, allowProposedApi: true, // Required by @xterm/addon-search to enable search functionality and decorations ignoreBracketedPasteMode: !termAllowBPM, macOptionIsMeta: termMacOptionIsMeta, + lineHeight: 1.2, }, { keydownHandler: model.handleTerminalKeydown.bind(model), From 58fbaac12ab413a5ef696cce0c0716a5b8ce98f7 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:13:55 -0800 Subject: [PATCH 18/32] Increase bold font weight to 700 for better visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed bold font weight from 600 to 700 to make bold text more prominent and closer to macOS Terminal appearance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/term/term.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index e7efddb0e..e172e715a 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -272,7 +272,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "SF Mono, Menlo, Monaco, Courier New, monospace", drawBoldTextInBrightColors: false, fontWeight: "400", - fontWeightBold: "600", + fontWeightBold: "700", allowTransparency: true, scrollback: termScrollback, allowProposedApi: true, // Required by @xterm/addon-search to enable search functionality and decorations From 855788d460f4fe8f13247adc53bea53c2acf8859 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:15:07 -0800 Subject: [PATCH 19/32] Revert "Increase bold font weight to 700 for better visibility" This reverts commit b92c35730fa1d8b3a6611b4d3fd044320d88e918. --- frontend/app/view/term/term.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index e172e715a..e7efddb0e 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -272,7 +272,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "SF Mono, Menlo, Monaco, Courier New, monospace", drawBoldTextInBrightColors: false, fontWeight: "400", - fontWeightBold: "700", + fontWeightBold: "600", allowTransparency: true, scrollback: termScrollback, allowProposedApi: true, // Required by @xterm/addon-search to enable search functionality and decorations From c3be1cf94e4efa9f87249dda3b3533c0d998cf72 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:15:07 -0800 Subject: [PATCH 20/32] Revert "Adjust font rendering to match macOS Terminal appearance" This reverts commit c217bcddabb94d21918750b4363a3134c1a5fd52. --- frontend/app/view/term/term.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index e7efddb0e..ed9a6cb65 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -271,14 +271,13 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => fontSize: termFontSize, fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "SF Mono, Menlo, Monaco, Courier New, monospace", drawBoldTextInBrightColors: false, - fontWeight: "400", - fontWeightBold: "600", + fontWeight: "normal", + fontWeightBold: "bold", allowTransparency: true, scrollback: termScrollback, allowProposedApi: true, // Required by @xterm/addon-search to enable search functionality and decorations ignoreBracketedPasteMode: !termAllowBPM, macOptionIsMeta: termMacOptionIsMeta, - lineHeight: 1.2, }, { keydownHandler: model.handleTerminalKeydown.bind(model), From 7e25cb6d3db0d186e593e97a8abf5edc5babcc4e Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:15:07 -0800 Subject: [PATCH 21/32] Revert "Use terminal theme background instead of transparent overlay" This reverts commit 445f0ef75926e3797fbce1b04a229e9fe8aa8f61. --- frontend/app/view/term/termutil.ts | 3 +-- pkg/wconfig/defaultconfig/presets.json | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/app/view/term/termutil.ts b/frontend/app/view/term/termutil.ts index 79fca1f74..8a7459e6c 100644 --- a/frontend/app/view/term/termutil.ts +++ b/frontend/app/view/term/termutil.ts @@ -34,8 +34,7 @@ export function computeTheme( } } let bgcolor = themeCopy.background; - // Don't make background transparent - use the theme's background color - // themeCopy.background = "#00000000"; + themeCopy.background = "#00000000"; return [themeCopy, bgcolor]; } diff --git a/pkg/wconfig/defaultconfig/presets.json b/pkg/wconfig/defaultconfig/presets.json index 91c4485d8..7586ab570 100644 --- a/pkg/wconfig/defaultconfig/presets.json +++ b/pkg/wconfig/defaultconfig/presets.json @@ -2,9 +2,7 @@ "bg@default": { "display:name": "Default", "display:order": -1, - "bg:*": true, - "bg": "white", - "bg:opacity": 1.0 + "bg:*": true }, "bg@white": { "display:name": "White", From e4d2df692dd83698e84a275b5da45736786b9979 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:15:07 -0800 Subject: [PATCH 22/32] Revert "Change default terminal font to SF Mono 11pt like macOS Terminal" This reverts commit 4cb6e9337a67d77ea361f6c470d3bf87be84e340. --- frontend/app/view/term/term-model.ts | 4 ++-- frontend/app/view/term/term.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index 203aecd3b..a1b9eb60d 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -243,9 +243,9 @@ export class TermViewModel implements ViewModel { const connName = blockData?.meta?.connection; const fullConfig = get(atoms.fullConfigAtom); const connFontSize = fullConfig?.connections?.[connName]?.["term:fontsize"]; - const rtnFontSize = blockData?.meta?.["term:fontsize"] ?? connFontSize ?? settingsFontSize ?? 11; + const rtnFontSize = blockData?.meta?.["term:fontsize"] ?? connFontSize ?? settingsFontSize ?? 12; if (typeof rtnFontSize != "number" || isNaN(rtnFontSize) || rtnFontSize < 4 || rtnFontSize > 64) { - return 11; + return 12; } return rtnFontSize; }); diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index ed9a6cb65..8104cc7f7 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -269,7 +269,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => { theme: termTheme, fontSize: termFontSize, - fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "SF Mono, Menlo, Monaco, Courier New, monospace", + fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "Hack", drawBoldTextInBrightColors: false, fontWeight: "normal", fontWeightBold: "bold", From 7d9a0a5569d6280d4c17a14e50dd95ee842e8cb4 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:15:07 -0800 Subject: [PATCH 23/32] Revert "Add macOS Basic light terminal theme" This reverts commit 2a2777b8ce94d9b776fadbf7e29c611d6a18d125. --- pkg/wconfig/defaultconfig/termthemes.json | 26 ----------------------- 1 file changed, 26 deletions(-) diff --git a/pkg/wconfig/defaultconfig/termthemes.json b/pkg/wconfig/defaultconfig/termthemes.json index 929b255fa..d0a667f0a 100644 --- a/pkg/wconfig/defaultconfig/termthemes.json +++ b/pkg/wconfig/defaultconfig/termthemes.json @@ -1,30 +1,4 @@ { - "macos-basic": { - "display:name": "macOS Basic", - "display:order": 0, - "black": "#000000", - "red": "#C23621", - "green": "#25BC24", - "yellow": "#ADAD27", - "blue": "#492EE1", - "magenta": "#D338D3", - "cyan": "#33BBC8", - "white": "#CBCCCD", - "brightBlack": "#818383", - "brightRed": "#FC391F", - "brightGreen": "#31E722", - "brightYellow": "#EAEC23", - "brightBlue": "#5833FF", - "brightMagenta": "#F935F8", - "brightCyan": "#14F0F0", - "brightWhite": "#E9EBEB", - "gray": "#818383", - "cmdtext": "#000000", - "foreground": "#000000", - "selectionBackground": "#B4D5FE", - "background": "#FFFFFF", - "cursor": "#000000" - }, "default-dark": { "display:name": "Default Dark", "display:order": 1, From 56e7c17e5f1c53a7d98302f868a20ede22f403c7 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 8 Dec 2025 00:15:07 -0800 Subject: [PATCH 24/32] Revert "Add White background preset option" This reverts commit 468dbfbd7648e216f6743593564e371c10b6c75b. --- pkg/wconfig/defaultconfig/presets.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkg/wconfig/defaultconfig/presets.json b/pkg/wconfig/defaultconfig/presets.json index 7586ab570..3d7cc1ad3 100644 --- a/pkg/wconfig/defaultconfig/presets.json +++ b/pkg/wconfig/defaultconfig/presets.json @@ -4,13 +4,6 @@ "display:order": -1, "bg:*": true }, - "bg@white": { - "display:name": "White", - "display:order": 1.0, - "bg:*": true, - "bg": "white", - "bg:opacity": 1.0 - }, "bg@rainbow": { "display:name": "Rainbow", "display:order": 2.1, From 069fa4c9a594280ff1abade4ace39df563590d34 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Wed, 10 Dec 2025 09:43:32 +0800 Subject: [PATCH 25/32] Preserve terminal scroll position during all writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent unwanted scroll-to-top when commands start executing (like Claude Code). Preserves scroll position if user has scrolled up, while still allowing auto-scroll when at bottom. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/term/termwrap.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index f0642e751..c80481e97 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -708,6 +708,11 @@ export class TermWrap { let prtn = new Promise((presolve, _) => { resolve = presolve; }); + + // Preserve scroll position if user has scrolled up + const wasAtBottom = this.terminal.buffer.active.baseY + this.terminal.rows >= this.terminal.buffer.active.length; + const scrollY = this.terminal.buffer.active.viewportY; + this.terminal.write(data, () => { if (setPtyOffset != null) { this.ptyOffset = setPtyOffset; @@ -716,6 +721,12 @@ export class TermWrap { this.dataBytesProcessed += data.length; } this.lastUpdated = Date.now(); + + // Restore scroll if user wasn't at bottom + if (!wasAtBottom && scrollY > 0) { + this.terminal.scrollToLine(scrollY); + } + resolve(); }); return prtn; From a378f7b2c63046720ff4374829417a8454ce7cda Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Wed, 10 Dec 2025 09:44:47 +0800 Subject: [PATCH 26/32] Revert "Preserve terminal scroll position during all writes" This reverts commit 8f313955259f7da7cc65d19c27675843bdc9c2c5. --- frontend/app/view/term/termwrap.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index c80481e97..f0642e751 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -708,11 +708,6 @@ export class TermWrap { let prtn = new Promise((presolve, _) => { resolve = presolve; }); - - // Preserve scroll position if user has scrolled up - const wasAtBottom = this.terminal.buffer.active.baseY + this.terminal.rows >= this.terminal.buffer.active.length; - const scrollY = this.terminal.buffer.active.viewportY; - this.terminal.write(data, () => { if (setPtyOffset != null) { this.ptyOffset = setPtyOffset; @@ -721,12 +716,6 @@ export class TermWrap { this.dataBytesProcessed += data.length; } this.lastUpdated = Date.now(); - - // Restore scroll if user wasn't at bottom - if (!wasAtBottom && scrollY > 0) { - this.terminal.scrollToLine(scrollY); - } - resolve(); }); return prtn; From fe684c96a4723b3901a2bb33be7182bd9eb9dbc4 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Fri, 12 Dec 2025 09:05:33 +0700 Subject: [PATCH 27/32] Add Cmd+Up/Down hotkeys to scroll terminal to top/bottom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cmd+Down: Scroll to bottom of terminal - Cmd+Up: Scroll to top of terminal - Quick navigation for long terminal output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/term/term-model.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index a1b9eb60d..72e45439e 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -489,6 +489,20 @@ export class TermViewModel implements ViewModel { this.setTermMode(newTermMode); return true; } + if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowDown")) { + // Scroll to bottom + if (this.termRef?.current?.terminal) { + this.termRef.current.terminal.scrollToBottom(); + } + return true; + } + if (keyutil.checkKeyPressed(waveEvent, "Cmd:ArrowUp")) { + // Scroll to top + if (this.termRef?.current?.terminal) { + this.termRef.current.terminal.scrollToLine(0); + } + return true; + } const blockData = globalStore.get(this.blockAtom); if (blockData.meta?.["term:mode"] == "vdom") { const vdomModel = this.getVDomModel(); From 6c3404fa54058c8cf6f019af3a86dc316acc7cd5 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Fri, 12 Dec 2025 09:48:38 +0700 Subject: [PATCH 28/32] Hide WaveAI button from top menu bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed the AI sparkle button from the tab bar. AI panel can still be accessed via other methods if needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/tab/tabbar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 7831fda1d..aeb88ca52 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -697,7 +697,6 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
)} -
From def8d03c8f817f13c9fcaa9fc3ded7dac947d59c Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Fri, 12 Dec 2025 18:18:51 +0700 Subject: [PATCH 29/32] Update generated RPC client files for tab title feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-generated TypeScript and Go RPC client code after rebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/store/wshclientapi.ts | 5 + frontend/types/gotypes.d.ts | 2 + package-lock.json | 153 ++++++++++------------------- pkg/wshrpc/wshclient/wshclient.go | 6 ++ 4 files changed, 66 insertions(+), 100 deletions(-) diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 59945312d..28a6d332d 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -292,6 +292,11 @@ class RpcApiType { return client.wshRpcCall("focuswindow", data, opts); } + // command "generatetabtitle" [call] + GenerateTabTitleCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { + return client.wshRpcCall("generatetabtitle", data, opts); + } + // command "getbuilderoutput" [call] GetBuilderOutputCommand(client: WshClient, data: string, opts?: RpcOpts): Promise { return client.wshRpcCall("getbuilderoutput", data, opts); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 23a900839..56b9253b3 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1222,6 +1222,7 @@ declare global { "settings:customsettings"?: number; "settings:customaimodes"?: number; "settings:secretscount"?: number; + "settings:transparent"?: boolean; "activity:activeminutes"?: number; "activity:fgminutes"?: number; "activity:openminutes"?: number; @@ -1306,6 +1307,7 @@ declare global { "settings:customsettings"?: number; "settings:customaimodes"?: number; "settings:secretscount"?: number; + "settings:transparent"?: boolean; }; // waveobj.Tab diff --git a/package-lock.json b/package-lock.json index 811475533..a2222c504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -192,6 +192,7 @@ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -212,6 +213,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -471,6 +473,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.37.0.tgz", "integrity": "sha512-DAFVUvEg+u7jUs6BZiVz9zdaUebYULPiQ4LM2R4n8Nujzyj7BZzGr2DCd85ip4p/cx7nAZWKM8pLcGtkTRTdsg==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.37.0", "@algolia/requester-browser-xhr": "5.37.0", @@ -624,6 +627,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2482,6 +2486,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2504,6 +2509,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2613,6 +2619,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3005,6 +3012,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4078,6 +4086,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.1.tgz", "integrity": "sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.8.1", "@docusaurus/logger": "3.8.1", @@ -5425,7 +5434,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -5447,7 +5455,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -6143,7 +6150,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6167,7 +6173,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6191,7 +6196,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6209,7 +6213,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6227,7 +6230,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6245,7 +6247,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6263,7 +6264,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6281,7 +6281,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6299,7 +6298,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6317,7 +6315,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6335,7 +6332,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -6353,7 +6349,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6377,7 +6372,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6401,7 +6395,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6425,7 +6418,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6449,7 +6441,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6473,7 +6464,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6497,7 +6487,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6518,7 +6507,6 @@ "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.4.4" }, @@ -6542,7 +6530,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6563,7 +6550,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6584,7 +6570,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -6857,6 +6842,7 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -8657,6 +8643,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -9051,7 +9038,8 @@ "version": "0.0.7", "resolved": "https://registry.npmjs.org/@table-nav/core/-/core-0.0.7.tgz", "integrity": "sha512-pCh18jHDRe3tw9sJZXfKi4cSD6VjHbn40CYdqhp5X91SIX7rakDEQAsTx6F7Fv9TUv265l+5rUDcYNaJ0N0cqQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@table-nav/react": { "version": "0.0.7", @@ -10098,6 +10086,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.2.tgz", "integrity": "sha512-lif9hF9afNk39jMUVYk5eyYEojLZQqaYX61LfuwUJJ1+qiQbh7jVaZXskYgzyjAIFDFQRf5Sd6MVM7EyXkfiRw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -10173,6 +10162,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -10459,6 +10449,7 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -11133,7 +11124,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", @@ -11188,6 +11180,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -11296,6 +11289,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -11360,6 +11354,7 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.37.0.tgz", "integrity": "sha512-y7gau/ZOQDqoInTQp0IwTOjkrHc4Aq4R8JgpmCleFwiLl+PbN2DMWoDUWZnrK8AhNJwT++dn28Bt4NZYNLAmuA==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.3.0", "@algolia/client-abtesting": "5.37.0", @@ -12191,6 +12186,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.2", "caniuse-lite": "^1.0.30001741", @@ -12759,6 +12755,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -13653,8 +13650,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -13822,6 +13818,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -14152,6 +14149,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -14561,6 +14559,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -14990,6 +14989,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -15465,7 +15465,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -15486,7 +15485,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -15502,7 +15500,6 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -15513,7 +15510,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -15827,6 +15823,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -22921,7 +22918,8 @@ "version": "0.52.2", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/monaco-languageserver-types": { "version": "0.4.0", @@ -23615,7 +23613,8 @@ "version": "2.12.0", "resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.12.0.tgz", "integrity": "sha512-mWJ5MOkcZ/ljHwfLw8+bN0V9ziGCoNoqULcp994j5DTGNQvnkWKWkA7rnO29Kyew5AoHxUnJ4Ndqfcl0HSQjXg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/overlayscrollbars-react": { "version": "0.5.6", @@ -24390,6 +24389,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -25293,6 +25293,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -25889,7 +25890,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -25907,7 +25907,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -26159,6 +26158,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -26389,6 +26389,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -26437,6 +26438,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -26526,6 +26528,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -26554,6 +26557,7 @@ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", "license": "MIT", + "peer": true, "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -26591,6 +26595,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -28355,6 +28360,7 @@ "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -28521,6 +28527,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -28639,6 +28646,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -29039,61 +29047,6 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, - "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" - } - }, - "node_modules/sharp/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -30034,7 +29987,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", @@ -30062,7 +30014,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=16" } @@ -30117,7 +30068,8 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -30173,7 +30125,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -30215,7 +30166,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -30237,7 +30187,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -30251,7 +30200,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -30266,7 +30214,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -31411,6 +31358,7 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", + "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -32319,6 +32267,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -32575,6 +32524,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -32817,6 +32767,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -33753,6 +33704,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -33802,7 +33754,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "tsunami/frontend/node_modules/redux-thunk": { "version": "3.1.0", diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 0490b403f..489cb8453 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -356,6 +356,12 @@ func FocusWindowCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) er return err } +// command "generatetabtitle", wshserver.GenerateTabTitleCommand +func GenerateTabTitleCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (string, error) { + resp, err := sendRpcRequestCallHelper[string](w, "generatetabtitle", data, opts) + return resp, err +} + // command "getbuilderoutput", wshserver.GetBuilderOutputCommand func GetBuilderOutputCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) ([]string, error) { resp, err := sendRpcRequestCallHelper[[]string](w, "getbuilderoutput", data, opts) From c570bf39530d150968e67d28141dfc9c700bc8e0 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sat, 13 Dec 2025 16:25:59 +0700 Subject: [PATCH 30/32] Revert "Hide WaveAI button from top menu bar" This reverts commit a10900e74a2c88ccefc7b9eaaea259e4ca9fa9e6. --- frontend/app/tab/tabbar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index aeb88ca52..7831fda1d 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -697,6 +697,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
)} +
From 4970ac419c18ea2c1e236066e8153271a553bfe3 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sat, 13 Dec 2025 16:26:41 +0700 Subject: [PATCH 31/32] Reapply "Hide WaveAI button from top menu bar" This reverts commit 589cc0bf1f75a062823354e4d2b84d7be6af5c5a. --- frontend/app/tab/tabbar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 7831fda1d..aeb88ca52 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -697,7 +697,6 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
)} -
From 18ab50f9b1f13e28794724705016878871d673cc Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Sat, 13 Dec 2025 16:35:18 +0700 Subject: [PATCH 32/32] Address CodeRabbit review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Optimize CreateTab to avoid double workspace fetch (pass workspace to createTabObj) - Remove paste scroll preservation (user wants auto-scroll to bottom on paste) - Add constants for magic numbers in AI panel scroll (AUTO_SCROLL_DEBOUNCE_MS, SCROLL_BOTTOM_THRESHOLD_PX) - Add passive event listener for better scroll performance - Update comment in custom.d.ts to clarify getPathForFile implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/aipanel/aipanelmessages.tsx | 11 +++++++---- frontend/app/view/term/termwrap.ts | 12 ------------ frontend/types/custom.d.ts | 2 +- pkg/wcore/workspace.go | 18 ++++++++++++------ 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/frontend/app/aipanel/aipanelmessages.tsx b/frontend/app/aipanel/aipanelmessages.tsx index 8e32f2035..3920197cc 100644 --- a/frontend/app/aipanel/aipanelmessages.tsx +++ b/frontend/app/aipanel/aipanelmessages.tsx @@ -8,6 +8,9 @@ import { AIModeDropdown } from "./aimode"; import { type WaveUIMessage } from "./aitypes"; import { WaveAIModel } from "./waveai-model"; +const AUTO_SCROLL_DEBOUNCE_MS = 100; +const SCROLL_BOTTOM_THRESHOLD_PX = 50; + interface AIPanelMessagesProps { messages: WaveUIMessage[]; status: string; @@ -32,7 +35,7 @@ export const AIPanelMessages = memo(({ messages, status, onContextMenu }: AIPane userHasScrolledUp.current = false; setTimeout(() => { isAutoScrolling.current = false; - }, 100); + }, AUTO_SCROLL_DEBOUNCE_MS); } }; @@ -48,15 +51,15 @@ export const AIPanelMessages = memo(({ messages, status, onContextMenu }: AIPane const { scrollTop, scrollHeight, clientHeight } = container; const distanceFromBottom = scrollHeight - scrollTop - clientHeight; - // If user is more than 50px from the bottom, they've scrolled up - if (distanceFromBottom > 50) { + // If user is more than threshold from the bottom, they've scrolled up + if (distanceFromBottom > SCROLL_BOTTOM_THRESHOLD_PX) { userHasScrolledUp.current = true; } else { userHasScrolledUp.current = false; } }; - container.addEventListener("scroll", handleScroll); + container.addEventListener("scroll", handleScroll, { passive: true }); return () => container.removeEventListener("scroll", handleScroll); }, []); diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index f0642e751..b6abb0674 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -830,9 +830,6 @@ export class TermWrap { e?.preventDefault(); e?.stopPropagation(); - // Preserve scroll position before paste - const scrollY = this.terminal.buffer.active.viewportY; - try { const clipboardData = await extractAllClipboardData(e); let firstImage = true; @@ -849,15 +846,6 @@ export class TermWrap { this.terminal.paste(data.text); } } - - // Restore scroll position after paste if it changed unexpectedly - setTimeout(() => { - const currentScrollY = this.terminal.buffer.active.viewportY; - // Only restore if we've scrolled significantly (not just normal paste scroll) - if (currentScrollY < scrollY - 10) { - this.terminal.scrollToLine(scrollY); - } - }, 50); } catch (err) { console.error("Paste error:", err); } finally { diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index b43f31c8c..480c2063f 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -130,7 +130,7 @@ declare global { openBuilder: (appId?: string) => void; // open-builder setBuilderWindowAppId: (appId: string) => void; // set-builder-window-appid doRefresh: () => void; // do-refresh - getPathForFile: (file: File) => string; // get-path-for-file + getPathForFile: (file: File) => string; // uses webUtils.getPathForFile }; type ElectronContextMenuItem = { diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index b9eddb151..da4b68119 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -228,7 +228,7 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate } // The initial tab for the initial launch should be pinned - tab, err := createTabObj(ctx, workspaceId, tabName, pinned || isInitialLaunch, inheritedMeta) + tab, err := createTabObjWithWorkspace(ctx, ws, tabName, pinned || isInitialLaunch, inheritedMeta) if err != nil { return "", fmt.Errorf("error creating tab: %w", err) } @@ -270,11 +270,8 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate return tab.OID, nil } -func createTabObj(ctx context.Context, workspaceId string, name string, pinned bool, meta waveobj.MetaMapType) (*waveobj.Tab, error) { - ws, err := GetWorkspace(ctx, workspaceId) - if err != nil { - return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err) - } +// createTabObjWithWorkspace creates a tab object with an already-fetched workspace +func createTabObjWithWorkspace(ctx context.Context, ws *waveobj.Workspace, name string, pinned bool, meta waveobj.MetaMapType) (*waveobj.Tab, error) { layoutStateId := uuid.NewString() tab := &waveobj.Tab{ OID: uuid.NewString(), @@ -297,6 +294,15 @@ func createTabObj(ctx context.Context, workspaceId string, name string, pinned b return tab, nil } +// createTabObj is a wrapper that fetches the workspace and calls createTabObjWithWorkspace +func createTabObj(ctx context.Context, workspaceId string, name string, pinned bool, meta waveobj.MetaMapType) (*waveobj.Tab, error) { + ws, err := GetWorkspace(ctx, workspaceId) + if err != nil { + return nil, fmt.Errorf("workspace %s not found: %w", workspaceId, err) + } + return createTabObjWithWorkspace(ctx, ws, name, pinned, meta) +} + // Must delete all blocks individually first. // Also deletes LayoutState. // recursive: if true, will recursively close parent window, workspace, if they are empty.