diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml
new file mode 100644
index 0000000000..a40e424966
--- /dev/null
+++ b/.github/workflows/build-macos.yml
@@ -0,0 +1,54 @@
+name: Build macOS
+on:
+ workflow_dispatch:
+
+env:
+ GO_VERSION: "1.26.2"
+ NODE_VERSION: 22
+ NODE_OPTIONS: --max-old-space-size=4096
+
+jobs:
+ build:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Install Go
+ run: |
+ curl -sL "https://go.dev/dl/go${{ env.GO_VERSION }}.darwin-arm64.tar.gz" | tar -xzf - -C .
+ mv go golang-${{ env.GO_VERSION }}
+ echo "module golang" > golang-${{ env.GO_VERSION }}/go.mod
+
+ - name: Install Zig
+ run: |
+ curl -sL "https://ziglang.org/download/0.14.0/zig-macos-aarch64-0.14.0.tar.xz" | tar -xJf -
+ mv zig-macos-aarch64-0.14.0 zig-0.14.0
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+ cache-dependency-path: package-lock.json
+
+ - name: Install Task
+ uses: arduino/setup-task@v2
+ with:
+ version: 3.x
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Install deps
+ run: npm ci --no-audit --no-fund
+ env:
+ GIT_ASKPASS: "echo"
+ GIT_TERMINAL_PROMPT: "0"
+
+ - name: Build
+ run: task package
+ env:
+ USE_SYSTEM_FPM: true
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v5
+ with:
+ name: macos-build
+ path: make/
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 30a8979b9b..9adf8336d1 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -12,25 +12,7 @@
name: "CodeQL"
on:
- push:
- branches: ["main"]
- paths:
- - "**/*.go"
- - "**/*.ts"
- - "**/*.tsx"
- pull_request:
- branches: ["main"]
- paths:
- - "**/*.go"
- - "**/*.ts"
- - "**/*.tsx"
- types:
- - opened
- - synchronize
- - reopened
- - ready_for_review
- schedule:
- - cron: "36 5 * * 5"
+ workflow_dispatch:
env:
NODE_VERSION: 22
diff --git a/.github/workflows/deploy-docsite.yml b/.github/workflows/deploy-docsite.yml
deleted file mode 100644
index 092b024cb5..0000000000
--- a/.github/workflows/deploy-docsite.yml
+++ /dev/null
@@ -1,80 +0,0 @@
-name: Docsite CI/CD
-
-run-name: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'Build and Deploy' || 'Test Build' }} Docsite
-
-env:
- NODE_VERSION: 22
-
-on:
- push:
- branches:
- - main
- workflow_dispatch:
- # Also run any time a PR is opened targeting the docs
- pull_request:
- branches:
- - main
- types:
- - opened
- - synchronize
- - reopened
- - ready_for_review
- paths:
- - "docs/**"
- - ".github/workflows/deploy-docsite.yml"
- - "Taskfile.yml"
-
-jobs:
- build:
- name: Build Docsite
- runs-on: ubuntu-latest
- if: github.event.pull_request.draft == false
- steps:
- - uses: actions/checkout@v6
- with:
- fetch-depth: 0
- - uses: actions/setup-node@v6
- with:
- node-version: ${{env.NODE_VERSION}}
- cache: npm
- cache-dependency-path: package-lock.json
- - name: Install Task
- uses: arduino/setup-task@v2
- with:
- version: 3.x
- repo-token: ${{ secrets.GITHUB_TOKEN }}
- - uses: nick-fields/retry@v4
- name: npm ci
- with:
- command: npm ci --no-audit --no-fund
- retry_on: error
- max_attempts: 3
- timeout_minutes: 5
- - name: Build docsite
- run: task docsite:build:public
- - name: Upload Build Artifact
- # Only upload the build artifact when pushed to the main branch
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- uses: actions/upload-pages-artifact@v4
- with:
- path: docs/build
- deploy:
- name: Deploy to GitHub Pages
- # Only deploy when pushed to the main branch
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- needs: build
- # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
- permissions:
- pages: write # to deploy to Pages
- id-token: write # to verify the deployment originates from an appropriate source
-
- # Deploy to the github-pages environment
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
-
- runs-on: ubuntu-latest
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v5
diff --git a/.github/workflows/testdriver-build.yml b/.github/workflows/testdriver-build.yml
deleted file mode 100644
index da190073e6..0000000000
--- a/.github/workflows/testdriver-build.yml
+++ /dev/null
@@ -1,83 +0,0 @@
-name: TestDriver.ai Build
-
-on:
- push:
- branches:
- - main
- tags:
- - "v[0-9]+.[0-9]+.[0-9]+*"
- pull_request:
- # branches:
- # - main
- # paths-ignore:
- # - "docs/**"
- # - ".storybook/**"
- # - ".vscode/**"
- # - ".editorconfig"
- # - ".gitignore"
- # - ".prettierrc"
- # - ".eslintrc.js"
- # - "**/*.md"
- types:
- - opened
- - synchronize
- - reopened
- - ready_for_review
- schedule:
- - cron: 0 21 * * *
- workflow_dispatch: null
-
-env:
- GO_VERSION: "1.25.6"
- NODE_VERSION: 22
-
-permissions:
- contents: read # To allow the action to read repository contents
- pull-requests: write # To allow the action to create/update pull request comments
-
-jobs:
- build_and_upload:
- name: Build for TestDriver.ai
- runs-on: windows-latest
- if: github.event.pull_request.draft == false
- steps:
- - uses: actions/checkout@v6
-
- # General build dependencies
- - uses: actions/setup-go@v6
- with:
- go-version: ${{env.GO_VERSION}}
- - uses: actions/setup-node@v6
- with:
- node-version: ${{env.NODE_VERSION}}
- cache: npm
- cache-dependency-path: package-lock.json
- - uses: nick-fields/retry@v4
- name: npm ci
- with:
- command: npm ci --no-audit --no-fund
- retry_on: error
- max_attempts: 3
- timeout_minutes: 5
- - name: Install Task
- uses: arduino/setup-task@v2
- with:
- version: 3.x
- repo-token: ${{ secrets.GITHUB_TOKEN }}
- - name: Install Zig
- uses: mlugg/setup-zig@v2
-
- - name: Build
- run: task package
- env:
- USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
- CSC_IDENTITY_AUTO_DISCOVERY: false # disable codesign
- shell: powershell # electron-builder's Windows code signing package has some compatibility issues with pwsh, so we need to use Windows Powershell
-
- # Upload .exe as an artifact
- - name: Upload .exe artifact
- id: upload
- uses: actions/upload-artifact@v5
- with:
- name: windows-exe
- path: make/*.exe
diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml
deleted file mode 100644
index 9d51ec7659..0000000000
--- a/.github/workflows/testdriver.yml
+++ /dev/null
@@ -1,141 +0,0 @@
-name: TestDriver.ai Run
-
-on:
- workflow_run:
- workflows: ["TestDriver.ai Build"]
- types:
- - completed
-
-env:
- GO_VERSION: "1.25.6"
- NODE_VERSION: 22
-
-permissions:
- contents: read
- statuses: write
-
-jobs:
- context:
- runs-on: ubuntu-22.04
- steps:
- - name: Dump GitHub context
- env:
- GITHUB_CONTEXT: ${{ toJson(github) }}
- run: echo "$GITHUB_CONTEXT"
- - name: Dump job context
- env:
- JOB_CONTEXT: ${{ toJson(job) }}
- run: echo "$JOB_CONTEXT"
- - name: Dump steps context
- env:
- STEPS_CONTEXT: ${{ toJson(steps) }}
- run: echo "$STEPS_CONTEXT"
- - name: Dump runner context
- env:
- RUNNER_CONTEXT: ${{ toJson(runner) }}
- run: echo "$RUNNER_CONTEXT"
- - name: Dump strategy context
- env:
- STRATEGY_CONTEXT: ${{ toJson(strategy) }}
- run: echo "$STRATEGY_CONTEXT"
- - name: Dump matrix context
- env:
- MATRIX_CONTEXT: ${{ toJson(matrix) }}
- run: echo "$MATRIX_CONTEXT"
- run_testdriver:
- name: Run TestDriver.ai
- runs-on: windows-latest
- if: github.event.workflow_run.conclusion == 'success'
- steps:
- - uses: testdriverai/action@main
- id: testdriver
- env:
- FORCE_COLOR: "3"
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- key: ${{ secrets.DASHCAM_API }}
- prerun: |
- $headers = @{
- Authorization = "token ${{ secrets.GITHUB_TOKEN }}"
- }
-
- $downloadFolder = "./download"
- $artifactFileName = "waveterm.exe"
- $artifactFilePath = "$downloadFolder/$artifactFileName"
-
- Write-Host "Starting the artifact download process..."
-
- # Create the download directory if it doesn't exist
- if (-not (Test-Path -Path $downloadFolder)) {
- Write-Host "Creating download folder..."
- mkdir $downloadFolder
- } else {
- Write-Host "Download folder already exists."
- }
-
- # Fetch the artifact upload URL
- Write-Host "Fetching the artifact upload URL..."
- $artifactUrl = (Invoke-RestMethod -Uri "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}/artifacts" -Headers $headers).artifacts[0].archive_download_url
-
- if ($artifactUrl) {
- Write-Host "Artifact URL successfully fetched: $artifactUrl"
- } else {
- Write-Error "Failed to fetch the artifact URL."
- exit 1
- }
-
- # Download the artifact (zipped file)
- Write-Host "Starting artifact download..."
- $artifactZipPath = "$env:TEMP\artifact.zip"
- try {
- Invoke-WebRequest -Uri $artifactUrl `
- -Headers $headers `
- -OutFile $artifactZipPath `
- -MaximumRedirection 5
-
- Write-Host "Artifact downloaded successfully to $artifactZipPath"
- } catch {
- Write-Error "Error downloading artifact: $_"
- exit 1
- }
-
- # Unzip the artifact
- $artifactUnzipPath = "$env:TEMP\artifact"
- Write-Host "Unzipping the artifact to $artifactUnzipPath..."
- try {
- Expand-Archive -Path $artifactZipPath -DestinationPath $artifactUnzipPath -Force
- Write-Host "Artifact unzipped successfully to $artifactUnzipPath"
- } catch {
- Write-Error "Failed to unzip the artifact: $_"
- exit 1
- }
-
- # Find the installer or app executable
- $artifactInstallerPath = Get-ChildItem -Path $artifactUnzipPath -Filter *.exe -Recurse | Select-Object -First 1
-
- if ($artifactInstallerPath) {
- Write-Host "Executable file found: $($artifactInstallerPath.FullName)"
- } else {
- Write-Error "Executable file not found. Exiting."
- exit 1
- }
-
- # Run the installer and log the result
- Write-Host "Running the installer: $($artifactInstallerPath.FullName)..."
- try {
- Start-Process -FilePath $artifactInstallerPath.FullName -Wait
- Write-Host "Installer ran successfully."
- } catch {
- Write-Error "Failed to run the installer: $_"
- exit 1
- }
-
- # Optional: If the app executable is different from the installer, find and launch it
- $wavePath = Join-Path $env:USERPROFILE "AppData\Local\Programs\waveterm\Wave.exe"
-
- Write-Host "Launching the application: $($wavePath)"
- Start-Process -FilePath $wavePath
- Write-Host "Application launched."
-
- prompt: |
- 1. /run testdriver/onboarding.yml
diff --git a/.gitignore b/.gitignore
index 7bd717e540..faa9b6deea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,6 @@ docsite/
.superpowers
docs/superpowers
.claude
+golang-*/
+zig-*/
+.pi/
diff --git a/.pi/context.md b/.pi/context.md
new file mode 100644
index 0000000000..c0f8aa764a
--- /dev/null
+++ b/.pi/context.md
@@ -0,0 +1,50 @@
+# Project Context
+
+## Problem Statement
+
+Most modern terminals and developer tools assume a **local-first workflow**:
+- Code lives on the local machine
+- Build tools run locally
+- Terminal access is to local shell or occasional remote SSH sessions
+- AI assistants analyze local files and local terminal output
+
+This doesn't match how many developers actually work:
+- Code lives on remote servers, cloud VMs, or containers
+- Builds happen remotely (CI/CD, remote compile farms)
+- The developer's machine is primarily a thin client
+- Network connectivity is the primary bottleneck, not local CPU
+
+## What This Fork Targets
+
+A terminal where **remote is the default**, not an afterthought:
+- SSH connections are first-class, not a plugin
+- Port forwarding is automatic from SSH config
+- File editing on remote machines feels as seamless as local
+- Durable sessions survive network interruptions gracefully
+- The terminal understands remote context (which host, which directory, which project)
+- Local resources (AI, file previews) enhance remote work rather than competing with it
+
+## What's Different from Upstream
+
+Wave Terminal already has excellent SSH and durable session support. This fork will:
+
+| Area | Upstream Wave | This Fork |
+|------|---------------|-----------|
+| **Port forwarding** | Not supported from SSH config | Automatic from `~/.ssh/config` |
+| **Local-first features** | AI, widgets, file previews for local | Evaluate which to keep/diminish |
+| **Remote context** | Basic | Potentially enhanced (host-aware prompts, etc.) |
+| **UI chrome** | Full Wave branding/chrome | Potentially stripped for remote-dev focus |
+
+## Non-Goals
+
+- Rebuild from scratch — this is a fork, not a rewrite
+- Remove all local features indiscriminately — evaluate usefulness
+- Compete with upstream — this is a specialized variant
+
+## Target User
+
+Developers who:
+- Spend >50% of terminal time in SSH sessions
+- Have multiple persistent remote development environments
+- Use `~/.ssh/config` extensively for host/forwarding configuration
+- Want a terminal that treats remote machines as "primary" workspaces
diff --git a/.pi/decisions.md b/.pi/decisions.md
new file mode 100644
index 0000000000..853224ab51
--- /dev/null
+++ b/.pi/decisions.md
@@ -0,0 +1,122 @@
+# Architecture Decisions
+
+## 2026-05-10: Fork Purpose
+
+**Decision:** Fork Wave Terminal to create a remote-development-optimized variant.
+
+**Context:** Most terminals assume local-first workflows. This fork treats remote SSH environments as primary workspaces.
+
+**Consequences:**
+- Upstream remains the base; we merge regularly
+- Features evaluated against "remote-first" usefulness
+- Local-first features may be removed/diminished if they conflict with remote workflow
+
+## 2026-05-10: `.pi/` as Planning Hub
+
+**Decision:** Use `.pi/` directory for all fork planning, specs, and agent context.
+
+**Context:** Keeps planning centralized and agent-accessible without cluttering the root or public docs.
+
+**Files:**
+- `.pi/index.md` — entry point
+- `.pi/context.md` — project background
+- `.pi/todos.md` — active tasks
+- `.pi/decisions.md` — this file
+- `.pi/specs/` — feature specifications
+
+## 2026-05-10: Port Forwarding — Config-First Approach
+
+**Decision:** Implement `LocalForward`/`RemoteForward` from `~/.ssh/config` and `connections.json`, not CLI flags.
+
+**Context:** SSH config is the standard place developers already define forwarding rules. Making Wave respect them is the least-surprise approach.
+
+**Approach:**
+1. Parse `LocalForward`/`RemoteForward` in `findSshConfigKeywords()`
+2. Add to `ConnKeywords` struct
+3. Return merged keywords from `ConnectToClient()`
+4. Start forwarding goroutines in `SSHConn.connectInternal()`
+5. Clean up listeners in `closeInternal_withlifecyclelock()`
+
+**Deferred:**
+- `DynamicForward` (needs SOCKS5 handler)
+- CLI flags on `wsh ssh` (can add later)
+- UI status indicator
+
+## 2026-05-14: Tab-Close Crash — Root Cause Found & Fixed
+
+**Decision:** Remove redundant `DestroyBlockController` goroutine from `CloseTab`; add `sync.Once` to `ShellProc.Close()` as defense-in-depth.
+
+**Context:** Investigation confirmed a race where `CloseTab` explicitly launched `DestroyBlockController` in a goroutine while `DeleteTab` → `DeleteBlock` → `BlockCloseEvent` triggered the same destruction again. This caused concurrent double-`Stop` on `ShellController` (with its Lock/Unlock/Relock window) and `DurableShellController` (which has no lock), leading to double `Session.Close()` / double `TerminateAndDetachJob`.
+
+**Fix applied:**
+1. `pkg/service/workspaceservice/workspaceservice.go` — removed the explicit `go DestroyBlockController()` loop; `DeleteTab` already triggers cleanup via events.
+2. `pkg/shellexec/shellexec.go` — added `closeOnce sync.Once` to `ShellProc` and wrapped `Close()` in `sp.closeOnce.Do`, preventing double `KillGraceful` / double goroutine spawn even if two Stops race.
+3. Added trace logging to `CloseTab`, `DestroyBlockController`, `ShellController.Stop`, `DurableShellController.Stop`, `handleBlockCloseEvent` for interactive diagnosis.
+4. Fixed 2 test-code panics (manual `close` of channel already closed by mock `KillGraceful`).
+
+**Consequences:**
+- `CloseTab` now has a single cleanup path: `DeleteTab` → `DeleteBlock` → event → `DestroyBlockController`
+- `ShellProc.Close()` is idempotent; any future code path that calls it twice is safe
+- 14 unit tests pass under `-race`
+
+## 2026-05-12: Secret Store — Keep
+
+**Decision:** Keep the secret store infrastructure; it's not AI-specific.
+
+**Context:** The secret store (`pkg/secretstore/`) is an encrypted key-value store backed by the OS keychain. It has three consumers:
+1. **AI API tokens** (`ai:apitokensecretname`) — going away with AI removal
+2. **SSH password auth** (`ssh:passwordsecretname`) — stays, useful for password-authenticated hosts
+3. **Wave App Store** — stays, general-purpose
+
+**Consequences:**
+- Remove `ai:apitokensecretname` field from `ConnKeywords` as part of AI cleanup
+- Keep `pkg/secretstore/`, `wsh secret` CLI, and `ssh:passwordsecretname` intact
+- Lightweight general infrastructure; useful for future features (e.g., file transfer credentials)
+
+## 2026-05-15: Claude Code Shell Integration — Analysis for Future Pi Agent Support
+
+**Finding:** Wave Terminal's Claude Code detection is built on top of a generic **shell integration protocol** (OSC 16162) that could be reused for pi coding agent support.
+
+### How Claude Code Integration Works
+
+| Layer | What it does | Relevant file |
+|-------|-------------|---------------|
+| **Shell integration protocol** | Custom OSC 16162 sequences injected into shell prompt. Sends command-start (`C`), command-done (`D`), shell-ready (`M`) events via base64-encoded payloads. | `frontend/app/view/term/osc-handlers.ts` |
+| **Command detection** | `isClaudeCodeCommand(decodedCmd)` checks if normalized command matches `/^claude\b/`. Also detects `opencode` with similar regex. | `frontend/app/view/term/osc-handlers.ts` |
+| **State atoms** | `shellIntegrationStatusAtom` (`"ready" \| "running-command" \| null`) and `claudeCodeActiveAtom` (`boolean`) track terminal state per block. | `frontend/app/view/term/termwrap.ts` |
+| **Visual indicator** | `getShellIntegrationIconButton()` in `term-model.ts` reads atoms and renders either generic sparkle icon or `TermClaudeIcon` (Anthropic SVG logo) with status tooltip. | `frontend/app/view/term/term-model.ts` |
+| **Telemetry gate** | `checkCommandForTelemetry()` filters out `ssh`, editors (`vim/nano/nvim`), `tail -f`, `claude`, and `opencode` from AI telemetry. | `frontend/app/view/term/osc-handlers.ts` |
+
+### What Was Removed Today
+
+- Sparkle icon + Claude logo from terminal block header (`getShellIntegrationIconButton` now returns `null`)
+- All tooltips referencing "Wave AI can run commands"
+- The `TermClaudeIcon` import from `term-model.ts`
+
+### What Remains (Dead Code, Phase D Cleanup)
+
+- `claudeCodeActiveAtom` in `termwrap.ts` — still set by OSC handlers, never read
+- `shellIntegrationStatusAtom` in `termwrap.ts` — still set by OSC handlers, never read
+- `isClaudeCodeCommand()` and `ClaudeCodeRegex` in `osc-handlers.ts` — still execute, results unused
+- `TermClaudeIcon` component in `term.tsx` — still exported, never imported
+- `checkCommandForTelemetry()` in `osc-handlers.ts` — still runs, telemetry already removed
+
+### Reuse Potential for Pi Coding Agent
+
+**The shell integration protocol itself is valuable** — it gives the terminal real-time awareness of:
+- When a command starts / finishes
+- What the command line is
+- Exit codes
+- Shell type and version
+- Whether the terminal is in an alternate buffer (e.g., `vim`, `less`)
+
+**For pi integration, we could:**
+1. Reuse the same OSC 16162 injection into `.bashrc`/`.zshrc`
+2. Add a `piActiveAtom` alongside `claudeCodeActiveAtom` with a `/^pi\b/` regex
+3. Show a pi icon in the terminal header when pi is the active command
+4. Use command-start/finish events to show "pi is running" status in the UI
+5. Use the alternate-buffer detection (`getBlockingCommand`) to suppress pi actions while inside `vim`/`less`/`ssh`
+
+**Key insight:** The protocol is generic AI-agent-agnostic infrastructure. The Claude-specific parts are just a regex (`/^claude\b/`) and an SVG icon. Replacing them with pi equivalents would be trivial if we want this later.
+
+**Decision:** Keep the underlying OSC 16162 shell integration infrastructure intact for now. Only the visual indicator (sparkle/Claude icon) and Wave-AI-specific tooltips were removed. If we want pi agent integration later, we can add `piActiveAtom` and a pi icon with minimal changes.
diff --git a/.pi/index.md b/.pi/index.md
new file mode 100644
index 0000000000..2d3e303ba5
--- /dev/null
+++ b/.pi/index.md
@@ -0,0 +1,28 @@
+# waveterm-remote Fork
+
+A fork of [Wave Terminal](https://github.com/wavetermdev/waveterm) optimized for **remote development workflows**.
+
+## Upstream
+
+- Original: `https://github.com/wavetermdev/waveterm`
+- This fork: `https://github.com/whoisjeremylam/waveterm-remote`
+- CWD origin points to this fork
+
+## Purpose
+
+Most developer terminals assume code is installed, built, and tested locally. This fork targets developers who primarily work on remote machines via SSH — with the local machine as a thin client.
+
+## Active Specs
+
+- [[specs/remove-telemetry.md]] — Remove all telemetry, analytics, and tracking
+- [[specs/remove-waveai.md]] — Remove/disable all Wave AI features
+- [[specs/portforwarding.md]] — SSH port forwarding (`LocalForward` / `RemoteForward`)
+
+## Context & Decisions
+
+- [[context.md]] — Full project background and goals
+- [[decisions.md]] — Architecture decisions (ADRs)
+
+## Tasks
+
+- [[todos.md]] — Active work and backlog
diff --git a/.pi/specs/bug-tabclose-crash.md b/.pi/specs/bug-tabclose-crash.md
new file mode 100644
index 0000000000..f4650c6b7c
--- /dev/null
+++ b/.pi/specs/bug-tabclose-crash.md
@@ -0,0 +1,442 @@
+# Bug: Crash on Tab Close After SSH Session Exit
+
+**Status:** Fixed (2026-05-14)
+**Priority:** High
+**Date:** 2026-05-13
+**Resolution:** Root cause confirmed; redundant goroutine removed from `CloseTab`; `ShellProc.Close()` made idempotent with `sync.Once`; trace logging added.
+
+## Reproduction Steps
+
+1. Connect to SSH from the dropdown
+2. Type `exit` in the shell
+3. Click the tab 'x' to close the tab
+4. → Crash
+
+## Thesis: Root Cause Analysis
+
+### Primary Suspect: Race Condition in `CloseTab` — Double Block Controller Destruction
+
+**Location:** `pkg/service/workspaceservice/workspaceservice.go:218-232`
+
+The `CloseTab` method has a critical design flaw that triggers **concurrent** `DestroyBlockController` calls for each block:
+
+```go
+func (svc *WorkspaceService) CloseTab(...) {
+ tab, err := wstore.DBGet[*waveobj.Tab](ctx, tabId)
+ if err == nil && tab != nil {
+ go func() { // ← Goroutine A
+ for _, blockId := range tab.BlockIds {
+ blockcontroller.DestroyBlockController(blockId)
+ }
+ }()
+ }
+ newActiveTabId, err := wcore.DeleteTab(ctx, ...) // ← Synchronous
+ // DeleteTab → DeleteBlock → sendBlockCloseEvent
+ // → handleBlockCloseEvent → go DestroyBlockController(blockId) ← Goroutine B
+}
+```
+
+Each block gets `DestroyBlockController` called **twice concurrently** — once from the explicit goroutine in `CloseTab` (Goroutine A), and once from the block-close event handler triggered by `DeleteBlock` (Goroutine B).
+
+### How This Leads to a Crash
+
+#### Path 1: `ShellController.Stop` — Double `ShellProc.Close()` on SSH Session
+
+`DestroyBlockController` calls `controller.Stop(true, Status_Done, true)`. For `ShellController`, `Stop` has a **Lock/Unlock/Relock** pattern that creates a race window:
+
+```go
+func (sc *ShellController) Stop(graceful bool, newStatus string, destroy bool) {
+ sc.Lock.Lock()
+ defer sc.Lock.Unlock()
+
+ if sc.ShellProc == nil || sc.ProcStatus == Status_Done || sc.ProcStatus == Status_Init {
+ return // ← Guard check, but...
+ }
+ sc.ShellProc.Close() // ← First Close
+ if graceful {
+ doneCh := sc.ShellProc.DoneCh
+ sc.Lock.Unlock() // ← UNLOCKS here, allowing concurrent Stop to enter
+ <-doneCh // ← Waits for shell process to finish
+ sc.Lock.Lock() // ← Re-locks after waiting
+ }
+ sc.ProcStatus = newStatus // ← Only NOW updated, but second Stop already entered
+}
+```
+
+**Race sequence:**
+1. Goroutine A calls `Stop`, acquires lock, checks `ProcStatus == Status_Running`, calls `ShellProc.Close()`, **unlocks** to wait on `doneCh`
+2. Goroutine B calls `Stop`, acquires lock, sees `ShellProc != nil` and `ProcStatus == Status_Running` (not yet updated), calls `ShellProc.Close()` **again**
+3. `ShellProc.Close()` calls `Cmd.KillGraceful()` → `SessionWrap.Kill()` → `Tty.Close()` + `Session.Close()`
+4. **Double `ssh.Session.Close()`** on a session that may already be closing (after user typed `exit`)
+5. **Double `PipePty.Close()`** — closing `os.File` descriptors twice
+
+On a closed/dying SSH session, `Session.Close()` sends a channel close message over a potentially-dead SSH mux. The `x/crypto/ssh` library's `channel.Close()` calls `channel.sendMessage()` which writes to the transport. If the mux loop has already exited and cleaned up, this can cause:
+- Panic from writing to a closed/cleaned-up channel
+- Panic from `close` on a closed channel (Go runtime panic)
+- Data race on mux internals that have been cleaned up
+
+#### Path 2: `ShellProc.Close()` Double Channel Close
+
+`ShellProc.Close()` spawns a goroutine:
+
+```go
+func (sp *ShellProc) Close() {
+ sp.Cmd.KillGraceful(DefaultGracefulKillWait)
+ go func() {
+ waitErr := sp.Cmd.Wait()
+ sp.SetWaitErrorAndSignalDone(waitErr)
+ if runtime.GOOS != "windows" {
+ sp.Cmd.Close()
+ }
+ }()
+}
+```
+
+When called twice concurrently, `KillGraceful` is called twice, and two goroutines are spawned that both call `Wait()` and `Close()`. While `Wait()` is protected by `sync.Once` and `SetWaitErrorAndSignalDone` is protected by `CloseOnce`, **`KillGraceful` and `Close` are NOT idempotent or protected**.
+
+For `SessionWrap`:
+- `KillGraceful` → `Kill()` → `Tty.Close()` + `Session.Close()` — called twice
+- `Close()` is a no-op (the `pty.Pty` interface has no `Close` method beyond `ReadWriteCloser`, and `SessionWrap` doesn't implement an explicit `Close()`)
+
+For `CmdWrap` (local shells):
+- `KillGraceful` → sends signal, then force-kills after timeout
+- `Close()` → `Cmd.Wait()` + pty close — double close on pty
+
+#### Path 3: Durable Shell — Job Termination Race with Block Deletion
+
+For SSH blocks using `DurableShellController` (the default for SSH connections):
+
+When user types `exit`:
+1. The remote shell process exits
+2. `HandleCmdJobExited` is called → `tryTerminateJobManager` terminates the job manager
+3. The output stream reaches EOF → `StreamDone = true`
+
+When user clicks tab X:
+- `DestroyBlockController` → `DurableShellController.Stop(true, Status_Done, true)` → `TerminateAndDetachJob(ctx, jobId)`
+- But the job may already be terminated/detached
+- `DetachJobFromBlock` tries to update the block's `JobId` field via `wstore.DBUpdateFn`
+- But the block may already be **deleted from the DB** by `DeleteBlock` (running concurrently in `DeleteTab`)
+- This could cause a DB error or panic if the update operates on a non-existent record
+
+#### Path 4: ConnMonitor Keepalive on Closing SSH Client
+
+When the SSH connection is still alive (connserver session persists after shell exit), `ConnMonitor` runs keepalive checks every 5 seconds:
+
+```go
+func (cm *ConnMonitor) SendKeepAlive() error {
+ client := cm.Client // ← Stale reference captured at creation time
+ if !cm.setKeepAliveInFlight() {
+ return nil
+ }
+ go func() {
+ _, _, err := client.SendRequest("keepalive@openssh.com", true, nil)
+ // ...
+ }()
+}
+```
+
+If `closeInternal_withlifecyclelock()` is called concurrently:
+1. It calls `conn.Monitor.Close()` (cancels context)
+2. It calls `client.Close()` (closes SSH client)
+3. It sets `conn.Client = nil`
+
+But a keepalive goroutine may have already captured `client := cm.Client` and started `SendRequest` on a closing/closed client. While `x/crypto/ssh` generally handles this gracefully (returning `io.EOF`), if the mux loop has already exited and cleaned up its internal channels, accessing the mux can race.
+
+### Secondary Contributing Factors
+
+1. **`DurableShellController.Stop` has no lock** — Unlike `ShellController.Stop`, concurrent calls can race on the `JobId` field read.
+
+2. **No idempotency guard on `ShellProc.Close()`** — No `sync.Once` or closed-flag prevents double-close.
+
+3. **`ConnMonitor` holds stale `*ssh.Client` reference** — Never updated when client is closed/nilled.
+
+4. **`handleBlockCloseEvent` launches goroutine** — `go DestroyBlockController(blockId)` makes the double-destroy race uncontrolled.
+
+### Most Likely Crash Sequence (SSH + Durable Shell)
+
+1. User types `exit` in SSH shell → shell process on remote exits
+2. `HandleCmdJobExited` fires → `tryTerminateJobManager` → job manager terminated
+3. User clicks tab X → `CloseTab` called
+4. Goroutine A: `DestroyBlockController(blockId)` → `DurableShellController.Stop()` → `TerminateAndDetachJob(jobId)`
+5. `DeleteTab` → `DeleteBlock` → `sendBlockCloseEvent`
+6. Goroutine B: `handleBlockCloseEvent` → `DestroyBlockController(blockId)` → second `DurableShellController.Stop()` → second `TerminateAndDetachJob(jobId)`
+7. First call terminates job and detaches from block (block's `JobId` cleared)
+8. Second call tries to detach from already-detached/non-existent block → potential DB error or nil pointer
+
+### Most Likely Crash Sequence (SSH + Non-Durable Shell)
+
+1. User types `exit` in SSH shell → `manageRunningShellProcess` wait loop detects exit → `ProcStatus = Status_Done`
+2. User clicks tab X → `CloseTab` called
+3. Goroutine A: `DestroyBlockController(blockId)` → `ShellController.Stop(true, Status_Done, true)` → sees `ProcStatus == Status_Done` → returns early (OK)
+4. Goroutine B (from `sendBlockCloseEvent`): `DestroyBlockController(blockId)` → controller already deleted from registry → returns early (OK)
+5. **But** if the timing is different — tab close happens while `ProcStatus` is still `Status_Running` (shell still exiting):
+ - Goroutine A: `Stop` acquires lock, sees Running, calls `ShellProc.Close()`, **unlocks** to wait
+ - Goroutine B: `Stop` acquires lock, sees Running (not yet Done), calls `ShellProc.Close()` **again**
+ - Double `Session.Close()` → potential panic in SSH library
+
+---
+
+## Logging Additions (Suggested)
+
+### 1. In `CloseTab` — Trace the double-destroy path
+
+**File:** `pkg/service/workspaceservice/workspaceservice.go`
+
+```go
+func (svc *WorkspaceService) CloseTab(ctx context.Context, workspaceId string, tabId string, fromElectron bool) (*CloseTabRtnType, waveobj.UpdatesRtnType, error) {
+ ctx = waveobj.ContextWithUpdates(ctx)
+ tab, err := wstore.DBGet[*waveobj.Tab](ctx, tabId)
+ if err == nil && tab != nil {
+ log.Printf("[closetab] tab=%s blocks=%v launching async DestroyBlockController goroutine", tabId, tab.BlockIds)
+ go func() {
+ for _, blockId := range tab.BlockIds {
+ log.Printf("[closetab] DestroyBlockController block=%s (from CloseTab goroutine)", blockId)
+ blockcontroller.DestroyBlockController(blockId)
+ }
+ }()
+ }
+ // ...
+}
+```
+
+### 2. In `DestroyBlockController` — Detect double-destroy
+
+**File:** `pkg/blockcontroller/blockcontroller.go`
+
+```go
+func DestroyBlockController(blockId string) {
+ controller := getController(blockId)
+ if controller == nil {
+ log.Printf("[destroy] block=%s: controller already nil (possible double-destroy)", blockId)
+ return
+ }
+ log.Printf("[destroy] block=%s: stopping controller (type=%T, connName=%s)", blockId, controller, controller.GetConnName())
+ controller.Stop(true, Status_Done, true)
+ wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, blockId))
+ deleteController(blockId)
+ log.Printf("[destroy] block=%s: controller deleted from registry", blockId)
+}
+```
+
+### 3. In `ShellController.Stop` — Detect concurrent Stop and double-Close
+
+**File:** `pkg/blockcontroller/shellcontroller.go`
+
+```go
+func (sc *ShellController) Stop(graceful bool, newStatus string, destroy bool) {
+ sc.Lock.Lock()
+ defer sc.Lock.Unlock()
+ log.Printf("[shellcontroller] Stop block=%s procStatus=%s shellProcNil=%v destroy=%v", sc.BlockId, sc.ProcStatus, sc.ShellProc == nil, destroy)
+
+ if sc.ShellProc == nil || sc.ProcStatus == Status_Done || sc.ProcStatus == Status_Init {
+ if newStatus != sc.ProcStatus {
+ sc.ProcStatus = newStatus
+ sc.sendUpdate_nolock()
+ }
+ return
+ }
+ // ...
+ sc.ShellProc.Close()
+ if graceful {
+ doneCh := sc.ShellProc.DoneCh
+ sc.Lock.Unlock() // ← RACE WINDOW STARTS HERE
+ log.Printf("[shellcontroller] Stop block=%s waiting on DoneCh (lock released)", sc.BlockId)
+ <-doneCh
+ sc.Lock.Lock() // ← RACE WINDOW ENDS HERE
+ log.Printf("[shellcontroller] Stop block=%s DoneCh closed (lock reacquired)", sc.BlockId)
+ }
+ // ...
+}
+```
+
+### 4. In `DurableShellController.Stop` — Log concurrent access
+
+**File:** `pkg/blockcontroller/durableshellcontroller.go`
+
+```go
+func (dsc *DurableShellController) Stop(graceful bool, newStatus string, destroy bool) {
+ if !destroy {
+ return
+ }
+ jobId := dsc.getJobId()
+ log.Printf("[durableshellcontroller] Stop block=%s jobId=%s destroy=%v", dsc.BlockId, jobId, destroy)
+ if jobId == "" {
+ return
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ jobcontroller.TerminateAndDetachJob(ctx, jobId)
+}
+```
+
+### 5. In `ConnMonitor.SendKeepAlive` — Detect stale client reference
+
+**File:** `pkg/remote/conncontroller/connmonitor.go`
+
+```go
+func (cm *ConnMonitor) SendKeepAlive() error {
+ client := cm.Client
+ currentClient := cm.Conn.GetClient()
+ if currentClient == nil {
+ log.Printf("[connmonitor] SendKeepAlive: conn=%s client is nil (connection closed)", cm.Conn.GetName())
+ return nil
+ }
+ if client != currentClient {
+ log.Printf("[connmonitor] SendKeepAlive: conn=%s stale client reference (monitor client != current client)", cm.Conn.GetName())
+ return nil
+ }
+ // ... rest of SendKeepAlive
+}
+```
+
+### 6. In `handleBlockCloseEvent` — Log the event handling
+
+**File:** `pkg/blockcontroller/blockcontroller.go`
+
+```go
+func handleBlockCloseEvent(event *wps.WaveEvent) {
+ blockId, ok := event.Data.(string)
+ if !ok {
+ log.Printf("[blockclose] invalid event data type")
+ return
+ }
+ log.Printf("[blockclose] block=%s: launching DestroyBlockController goroutine from event handler", blockId)
+ go DestroyBlockController(blockId)
+}
+```
+
+**File:** `pkg/jobcontroller/jobcontroller.go`
+
+```go
+func handleBlockCloseEvent(event *wps.WaveEvent) {
+ // ... existing code ...
+ log.Printf("[blockclose-job] block=%s: found %d jobs to terminate", blockId, len(jobIds))
+ for _, jobId := range jobIds {
+ log.Printf("[blockclose-job] block=%s: terminating job=%s", blockId, jobId)
+ TerminateAndDetachJob(ctx, jobId)
+ }
+}
+```
+
+---
+
+## Tests Written
+
+**File:** `pkg/blockcontroller/blockcontroller_test.go`
+
+### Tests Implemented
+
+| Test | What it tests | Result |
+|------|--------------|--------|
+| `TestShellControllerStopConcurrent/double_stop_does_not_double_kill` | Two concurrent `Stop` calls on a running ShellController — uses slow mock to expose Lock/Unlock/Relock race | **PASS** — but only checks KillGraceful count, not data race |
+| `TestShellControllerStopConcurrent/stop_after_proc_done_is_noop` | Stop on a controller with ProcStatus=Done | **PASS** |
+| `TestShellControllerStopConcurrent/stop_sets_status_done` | Stop updates ProcStatus correctly | **PASS** |
+| `TestDestroyBlockControllerDoubleCall` | Two concurrent `DestroyBlockController` calls for same blockId | **PASS** — second call finds nil controller and returns |
+| `TestDestroyBlockControllerDoubleCallDurable` | Same test with DurableShellController | **PASS** |
+| `TestDurableShellControllerStopConcurrent/stop_with_empty_jobid_is_noop` | Stop with no jobId | **PASS** |
+| `TestDurableShellControllerStopConcurrent/stop_without_destroy_is_noop` | Stop with destroy=false | **PASS** |
+| `TestShellControllerStopNilShellProc/nil_proc_updates_status` | Stop with nil ShellProc updates status | **PASS** |
+| `TestShellControllerStopNilShellProc/nil_proc_already_done_noop` | Stop when already Done | **PASS** |
+| `TestShellControllerStopNilShellProc/nil_proc_init_status` | Stop when Init | **PASS** |
+| `TestShellProcDoubleClose/double_close_on_running_proc` | Two concurrent `ShellProc.Close()` calls | **PASS** |
+| `TestShellProcDoubleClose/close_then_wait` | Close then second Close after Wait | **PASS** — Wait is protected by sync.Once |
+| `TestShellControllerStopRaceWithDoneStatus` | Tab-close Stop racing with shell-exit status update | **PASS** |
+| `TestShellControllerStopDoesNotPanicOnClosedSession/closed_session_stop` | Stop on closed SSH session | **PASS** |
+| `TestShellControllerStopDoesNotPanicOnClosedSession/concurrent_stop_on_closing_session` | Three concurrent operations: two Stops + shell exit | **PASS** |
+
+### Test Infrastructure
+
+- **`mockConnInterface`**: Fast mock where `Wait()` returns immediately. Good for testing the guard conditions and status updates.
+- **`slowMockConnInterface`**: Slow mock where `Wait()` blocks until `KillGraceful` signals it or `waitDone` is closed. Essential for exposing the Lock/Unlock/Relock race in `ShellController.Stop`.
+- **`mockClosedConnInterface`**: Mock that returns errors from all operations, simulating a closed SSH session.
+
+### Key Finding from Tests
+
+The `double_stop_does_not_double_kill` test **passes without detecting the double-KillGraceful** in the default case because:
+- With the slow mock, `KillGraceful` triggers `Wait()` to complete, and the `DoneCh` is signaled
+- The second `Stop` call sees `ProcStatus == Status_Done` (updated after the first Stop completes) and returns early
+- **However**, this depends on timing. If the second `Stop` enters during the `Lock.Unlock()` / `<-doneCh` / `Lock.Lock()` window, it WILL call `ShellProc.Close()` again
+
+Running with `go test -race` does not flag a data race in the test because the test's `Stop` calls are serialized by the `ShellController.Lock`. The actual race is a **logical race** (double-close), not a data race detectable by the race detector. The race detector would catch it if two goroutines accessed the same `ShellProc` fields without synchronization, but `ShellProc.Close()` is called under the controller's lock.
+
+**The real danger** is that `ShellProc.Close()` launches a **goroutine** (`go func() { waitErr := sp.Cmd.Wait(); ... }()`), and the second `Close()` launches another goroutine. Both goroutines call `Cmd.Wait()` and `Cmd.Close()` concurrently without synchronization. This IS a data race on the SSH session internals, but it happens inside the `x/crypto/ssh` library, not in waveterm code, so the Go race detector won't flag it directly.
+
+---
+
+## In Flight / Not Yet Done
+
+### Tests Still Needed
+
+1. **`go test -race` on the double-ShellProc.Close goroutine race** — The `ShellProc.Close()` method spawns a goroutine that calls `Cmd.Wait()` and `Cmd.Close()`. Two concurrent `Close()` calls spawn two goroutines that race on SSH session internals. Need a test that directly exercises this goroutine race.
+
+2. **Integration test with real SSH session** — Unit tests can't fully simulate the `x/crypto/ssh` library's behavior when `Session.Close()` is called on a closing session. An integration test with a real SSH connection would catch panics in the SSH library.
+
+3. **`CloseTab` double-destroy integration test** — Test the full `CloseTab` flow: create a tab with blocks, then close it, and verify no double-destroy or panic.
+
+4. **`ConnMonitor` keepalive on stale client test** — Test that `SendKeepAlive` on a closed/nilled client doesn't panic.
+
+5. **`DurableShellController.Stop` concurrent job termination test** — Test two concurrent `TerminateAndDetachJob` calls on the same jobId. This requires setting up the job DB, which is more complex.
+
+### Logging Not Yet Added
+
+The logging additions described above are **designed but not yet implemented** in the source files. They should be added to enable interactive crash reproduction and diagnosis.
+
+### Fix Not Yet Implemented
+
+The fix for the primary root cause (double-destroy in `CloseTab`) should be one of:
+
+**Option A: Remove the redundant goroutine in `CloseTab`**
+The explicit `go func() { DestroyBlockController(...) }()` goroutine in `CloseTab` is redundant because `DeleteTab` → `DeleteBlock` → `sendBlockCloseEvent` already triggers controller destruction. Removing it eliminates the double-destroy entirely.
+
+**Option B: Add idempotency to `DestroyBlockController`**
+Make `DestroyBlockController` safe for concurrent calls by adding a `destroyed` flag or using `sync.Once`:
+
+```go
+func DestroyBlockController(blockId string) {
+ // Use sync.Map or a separate set to track in-progress destructions
+ if !markDestroyInProgress(blockId) {
+ return // already being destroyed
+ }
+ defer clearDestroyInProgress(blockId)
+ controller := getController(blockId)
+ if controller == nil {
+ return
+ }
+ controller.Stop(true, Status_Done, true)
+ wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, blockId))
+ deleteController(blockId)
+}
+```
+
+**Option C: Make `ShellProc.Close()` idempotent**
+Add a `sync.Once` to `ShellProc.Close()`:
+
+```go
+func (sp *ShellProc) Close() {
+ sp.closeOnce.Do(func() {
+ sp.Cmd.KillGraceful(DefaultGracefulKillWait)
+ go func() {
+ defer func() {
+ panichandler.PanicHandler("ShellProc.Close", recover())
+ }()
+ waitErr := sp.Cmd.Wait()
+ sp.SetWaitErrorAndSignalDone(waitErr)
+ if runtime.GOOS != "windows" {
+ sp.Cmd.Close()
+ }
+ }()
+ })
+}
+```
+
+**Recommended approach:** Option A (remove redundant goroutine) + Option C (make ShellProc.Close idempotent) as defense-in-depth. Option A fixes the root cause; Option C protects against any other code path that might call Close twice.
+
+### Interactive Reproduction Needed
+
+The tests above confirm the structural race conditions exist but don't reproduce the actual crash. To confirm the crash:
+1. Add the logging additions
+2. Build and run waveterm with `task dev`
+3. Connect to an SSH server from the dropdown
+4. Type `exit` in the shell
+5. Click the tab X
+6. Check logs for double-destroy patterns and any panic/crash output
\ No newline at end of file
diff --git a/.pi/specs/portforwarding.md b/.pi/specs/portforwarding.md
new file mode 100644
index 0000000000..4855522b10
--- /dev/null
+++ b/.pi/specs/portforwarding.md
@@ -0,0 +1,417 @@
+# SSH Port Forwarding Implementation Spec
+
+## Problem
+
+Wave Terminal parses `~/.ssh/config` for connection settings but ignores `LocalForward`, `RemoteForward`, and `DynamicForward` directives. Users who define port forwarding rules in their SSH config get no forwarding when connecting through Wave.
+
+## Scope
+
+- **In scope**: `LocalForward` and `RemoteForward` parsed from `~/.ssh/config` and `connections.json`
+- **Out of scope**: `DynamicForward` (requires SOCKS5 handler not in stdlib), CLI flags on `wsh ssh`, UI status indicators
+
+## Current Architecture
+
+```
+~/.ssh/config ──┐
+ │
+connections.json ─┼──→ findSshConfigKeywords() / ConnKeywords struct
+ │
+wsh ssh flags ─┘
+ │
+ ▼
+ ConnectToClient() — merges keywords, creates *ssh.Client
+ │
+ ▼
+ SSHConn.connectInternal() — stores client, starts monitor/wsh
+ │
+ ▼
+ SSHConn.Close() — tears down client, monitor, domain socket
+```
+
+The merged `ConnKeywords` are consumed inside `ConnectToClient()` to build `ssh.ClientConfig` and are **never returned** to the caller. `conncontroller` only receives `connFlags` (CLI/frontend flags), not the full merged config.
+
+## Changes
+
+### 1. `pkg/wconfig/settingsconfig.go` — ConnKeywords struct
+
+Add two fields to `ConnKeywords`:
+
+```go
+SshLocalForward []string `json:"ssh:localforward,omitempty"`
+SshRemoteForward []string `json:"ssh:remoteforward,omitempty"`
+```
+
+Placement: after `SshGlobalKnownHostsFile`, before the closing `}`.
+
+### 2. `pkg/remote/sshclient.go` — Config parsing
+
+#### 2a. `findSshConfigKeywords()` — Parse from `~/.ssh/config`
+
+Add after the `GlobalKnownHostsFile` parsing block (before the `return`):
+
+```go
+localForwardRaw := WaveSshConfigUserSettings().GetAll(hostPattern, "LocalForward")
+for i := 0; i < len(localForwardRaw); i++ {
+ localForwardRaw[i] = trimquotes.TryTrimQuotes(localForwardRaw[i])
+}
+sshKeywords.SshLocalForward = localForwardRaw
+
+remoteForwardRaw := WaveSshConfigUserSettings().GetAll(hostPattern, "RemoteForward")
+for i := 0; i < len(remoteForwardRaw); i++ {
+ remoteForwardRaw[i] = trimquotes.TryTrimQuotes(remoteForwardRaw[i])
+}
+sshKeywords.SshRemoteForward = remoteForwardRaw
+```
+
+This follows the exact pattern used for `IdentityFile` (multi-value keyword via `GetAll` + quote trimming).
+
+#### 2b. `findSshDefaults()` — Default values
+
+Add to the defaults function:
+
+```go
+sshKeywords.SshLocalForward = []string{}
+sshKeywords.SshRemoteForward = []string{}
+```
+
+#### 2c. `mergeKeywords()` — Cascade merging
+
+Add to the merge function (follows the `SshProxyJump` pattern):
+
+```go
+if newKeywords.SshLocalForward != nil {
+ outKeywords.SshLocalForward = newKeywords.SshLocalForward
+}
+if newKeywords.SshRemoteForward != nil {
+ outKeywords.SshRemoteForward = newKeywords.SshRemoteForward
+}
+```
+
+#### 2d. `ConnectToClient()` — Return merged keywords
+
+Change the signature from:
+
+```go
+func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wconfig.ConnKeywords) (*ssh.Client, int32, error)
+```
+
+To:
+
+```go
+func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wconfig.ConnKeywords) (*ssh.Client, int32, *wconfig.ConnKeywords, error)
+```
+
+The `sshKeywords` variable already exists at the point of the final return. Change all return statements:
+
+- `return nil, jumpNum, ConnectionError{...}` → `return nil, jumpNum, nil, ConnectionError{...}`
+- `return client, debugInfo.JumpNum, nil` → `return client, debugInfo.JumpNum, sshKeywords, nil`
+- `return nil, debugInfo.JumpNum, ConnectionError{...}` → `return nil, debugInfo.JumpNum, nil, ConnectionError{...}`
+
+### 3. `pkg/remote/conncontroller/conncontroller.go` — Runtime forwarding
+
+#### 3a. `SSHConn` struct — Store forwarding state
+
+Add fields:
+
+```go
+LocalForwardListeners []net.Listener // local listeners for LocalForward
+RemoteForwardListeners []net.Listener // remote listeners (from client.Listen) for RemoteForward
+```
+
+#### 3b. Forwarding setup function
+
+Add a new unexported method:
+
+```go
+func (conn *SSHConn) startPortForwarding(ctx context.Context, keywords *wconfig.ConnKeywords) {
+ client := conn.GetClient()
+ if client == nil {
+ return
+ }
+
+ // LocalForward: listen locally, dial through SSH to remote
+ for _, fwd := range keywords.SshLocalForward {
+ parts := strings.Fields(fwd)
+ if len(parts) != 2 {
+ conn.Infof(ctx, "LocalForward: skipping malformed rule: %q\n", fwd)
+ continue
+ }
+ bindAddr, dest := parts[0], parts[1]
+ go func() {
+ defer panichandler.PanicHandler("conncontroller:localforward", recover())
+ listener, err := net.Listen("tcp", bindAddr)
+ if err != nil {
+ conn.Infof(ctx, "LocalForward %s: failed to listen: %v\n", fwd, err)
+ return
+ }
+ conn.WithLock(func() {
+ conn.LocalForwardListeners = append(conn.LocalForwardListeners, listener)
+ })
+ conn.Infof(ctx, "LocalForward started: %s -> %s\n", bindAddr, dest)
+ for {
+ localConn, err := listener.Accept()
+ if err != nil {
+ return
+ }
+ bindAddr, dest := bindAddr, dest // capture for goroutine
+ go func() {
+ defer panichandler.PanicHandler("conncontroller:localforward-tunnel", recover())
+ remoteConn, err := client.Dial("tcp", dest)
+ if err != nil {
+ localConn.Close()
+ return
+ }
+ conn.MonitorUpdate(localConn, remoteConn)
+ io.CopyBoth(localConn, remoteConn)
+ localConn.Close()
+ remoteConn.Close()
+ }()
+ }
+ }()
+ }
+
+ // RemoteForward: listen on remote via SSH, dial locally
+ for _, fwd := range keywords.SshRemoteForward {
+ parts := strings.Fields(fwd)
+ if len(parts) != 2 {
+ conn.Infof(ctx, "RemoteForward: skipping malformed rule: %q\n", fwd)
+ continue
+ }
+ bindAddr, dest := parts[0], parts[1]
+ go func() {
+ defer panichandler.PanicHandler("conncontroller:remoteforward", recover())
+ listener, err := client.Listen("tcp", bindAddr)
+ if err != nil {
+ conn.Infof(ctx, "RemoteForward %s: failed to listen: %v\n", fwd, err)
+ return
+ }
+ conn.WithLock(func() {
+ conn.RemoteForwardListeners = append(conn.RemoteForwardListeners, listener)
+ })
+ conn.Infof(ctx, "RemoteForward started: %s -> %s\n", bindAddr, dest)
+ for {
+ remoteConn, err := listener.Accept()
+ if err != nil {
+ return
+ }
+ bindAddr, dest := bindAddr, dest // capture for goroutine
+ go func() {
+ defer panichandler.PanicHandler("conncontroller:remoteforward-tunnel", recover())
+ localConn, err := net.Dial("tcp", dest)
+ if err != nil {
+ remoteConn.Close()
+ return
+ }
+ conn.MonitorUpdate(remoteConn, localConn)
+ io.CopyBoth(localConn, remoteConn)
+ localConn.Close()
+ remoteConn.Close()
+ }()
+ }
+ }()
+ }
+}
+```
+
+Notes:
+- Follows the existing goroutine pattern: `defer panichandler.PanicHandler("...", recover())`
+- Listeners are stored on the struct via `conn.WithLock` for cleanup
+- Uses `conn.Infof` for debug logging (consistent with existing connection debug output)
+- `io.CopyBoth` for bidirectional tunneling (standard Go pattern)
+- Variable capture (`bindAddr, dest := bindAddr, dest`) prevents goroutine closure bugs
+
+#### 3c. `connectInternal()` — Call forwarding setup
+
+Change the `ConnectToClient` call to capture the merged keywords:
+
+```go
+client, _, sshKeywords, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags)
+```
+
+After the client is stored and the monitor is started, add:
+
+```go
+// Start port forwarding with merged SSH config keywords
+if sshKeywords != nil {
+ conn.startPortForwarding(ctx, sshKeywords)
+}
+```
+
+Placement: after the `conn.WithLock` block that sets `conn.Client` and `conn.Monitor`, before the `waitForDisconnect` goroutine.
+
+#### 3d. `closeInternal_withlifecyclelock()` — Cleanup
+
+Add listener cleanup before the existing `client.Close()` call:
+
+```go
+// Close local forward listeners
+conn.WithLock(func() {
+ for _, l := range conn.LocalForwardListeners {
+ l.Close()
+ }
+ conn.LocalForwardListeners = nil
+ for _, l := range conn.RemoteForwardListeners {
+ l.Close()
+ }
+ conn.RemoteForwardListeners = nil
+})
+```
+
+This runs inside `lifecycleLock`, before `client.Close()`, ensuring no new connections are accepted during teardown.
+
+### 4. Call site updates
+
+Every caller of `remote.ConnectToClient` must handle the new 4th return value.
+
+#### `pkg/remote/conncontroller/conncontroller.go`
+
+Already covered in 3c above.
+
+#### `cmd/test-conn/main-test-conn.go`
+
+Check for any direct calls:
+
+```bash
+grep -rn "ConnectToClient" /home/jeremy/projects/waveterm/
+```
+
+Update each call site to capture (or ignore with `_`) the new `*wconfig.ConnKeywords` return value.
+
+### 5. Tests
+
+#### `pkg/remote/sshclient_test.go` (new file)
+
+Table-driven tests for config parsing. No network required.
+
+```go
+package remote
+
+import "testing"
+
+func TestFindSshConfigKeywords_LocalForward(t *testing.T) {
+ t.Parallel()
+ // Uses a temp ~/.ssh/config with LocalForward directives
+ // Verifies SshLocalForward is populated correctly
+}
+
+func TestMergeKeywords_LocalForward(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ old *wconfig.ConnKeywords
+ new *wconfig.ConnKeywords
+ wantLocal []string
+ wantRemote []string
+ }{
+ {
+ name: "new overrides old",
+ old: &wconfig.ConnKeywords{SshLocalForward: []string{"8080 localhost:80"}},
+ new: &wconfig.ConnKeywords{SshLocalForward: []string{"9090 localhost:90"}},
+ wantLocal: []string{"9090 localhost:90"},
+ },
+ {
+ name: "nil new preserves old",
+ old: &wconfig.ConnKeywords{SshLocalForward: []string{"8080 localhost:80"}},
+ new: &wconfig.ConnKeywords{},
+ wantLocal: []string{"8080 localhost:80"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := mergeKeywords(tt.old, tt.new)
+ // assert got.SshLocalForward matches tt.wantLocal
+ })
+ }
+}
+```
+
+#### `pkg/remote/conncontroller/conncontroller_test.go` (new file)
+
+Integration-style test using `net.Listener` (no real SSH):
+
+```go
+package conncontroller
+
+func TestLocalForwardStartsAndStops(t *testing.T) {
+ // Create a mock SSHConn with a real net.Listener as the "remote"
+ // Verify LocalForward listener is created on startPortForwarding
+ // Verify it's closed on closeInternal_withlifecyclelock
+}
+```
+
+This follows the `sshagent_unix_test.go` pattern: real sockets, no SSH daemon.
+
+### 6. Documentation
+
+#### `docs/docs/connections.mdx`
+
+**SSH Config Parsing table** — Add rows:
+
+| Keyword | Description |
+|---------|-------------|
+| LocalForward | Can be specified multiple times. Format: `bind_address destination` (e.g., `8080 localhost:80` or `127.0.0.1:8080 localhost:80`). Listens on the local machine and forwards connections through the SSH tunnel to the remote destination. |
+| RemoteForward | Can be specified multiple times. Format: `bind_address destination` (e.g., `9090 localhost:3000`). Listens on the remote machine and forwards connections back to the local destination. Requires `AllowTcpForwarding` on the remote sshd. |
+
+**Internal SSH Configuration table** — Add rows:
+
+| Keyword | Description |
+|---------|-------------|
+| ssh:localforward | A list of strings for local port forwarding rules. Format: `"8080 localhost:80"`. Can be used to override or supplement `~/.ssh/config` values. |
+| ssh:remoteforward | A list of strings for remote port forwarding rules. Format: `"9090 localhost:3000"`. Can be used to override or supplement `~/.ssh/config` values. |
+
+**New example section** after "Example SSH Config Host":
+
+```markdown
+### Port Forwarding
+
+Port forwarding rules from `~/.ssh/config` are automatically applied when you connect through Wave:
+
+```
+Host myserver
+ User username
+ HostName 203.0.113.254
+ LocalForward 8080 localhost:80
+ RemoteForward 9090 localhost:3000
+```
+
+Connecting to `myserver` will listen on local port 8080 (forwarded to the remote's localhost:80) and listen on the remote's port 9090 (forwarded to your local localhost:3000).
+
+Port forwarding can also be defined entirely in `connections.json`:
+
+```json
+{
+ "myusername@myhost": {
+ "ssh:localforward": ["8080 localhost:80"],
+ "ssh:remoteforward": ["9090 localhost:3000"]
+ }
+}
+```
+```
+
+#### `docs/docs/releasenotes.mdx`
+
+Add entry under the current development version.
+
+## Error Handling
+
+- Malformed forwarding rules (wrong number of fields) are logged via `conn.Infof` and skipped — they never break the connection
+- Listener bind failures (port already in use) are logged — the connection proceeds without that specific forward
+- Tunnel dial failures log and close the individual connection — other tunnels and the SSH session continue
+- All forwarding goroutines use `panichandler.PanicHandler` to prevent crashes from propagating
+
+## Lifecycle
+
+| Event | Action |
+|-------|--------|
+| Connect starts | `ConnectToClient` returns merged keywords including forwarding rules |
+| Client established | `startPortForwarding` spawns goroutines, stores listeners on `SSHConn` |
+| Connection active | Tunnels run via `io.CopyBoth`; monitor tracks activity |
+| Disconnect starts | `closeInternal_withlifecyclelock` closes all forwarding listeners under `lifecycleLock` |
+| Client closes | `client.Close()` tears down remote listeners and all in-flight tunnels |
+
+## Out of Scope (Future)
+
+- **`DynamicForward`** — Requires a SOCKS5 proxy handler. The `golang.org/x/crypto/ssh` library has no built-in one. Would need a third-party package or custom ~200-line SOCKS5 implementation.
+- **`wsh ssh -L` / `-R` CLI flags** — Can be added to `wshcmd-ssh.go` later, following the existing `-i`/`-l`/`-p` flag pattern.
+- **UI status indicator** — A block header icon showing active port forwards (similar to the wsh icon).
+- **`GatewayPorts`** support — The `ssh` keyword for binding remote forwards to all interfaces.
diff --git a/.pi/specs/remove-telemetry.md b/.pi/specs/remove-telemetry.md
new file mode 100644
index 0000000000..30468a898a
--- /dev/null
+++ b/.pi/specs/remove-telemetry.md
@@ -0,0 +1,575 @@
+# Spec: Remove Telemetry
+
+**Date:** 2026-05-13
+**Status:** Implemented
+**Review:** `.pi/reviews/remove-telemetry-independent-review.md`
+
+## Goal
+
+Completely remove all telemetry, analytics, and user tracking from waveterm. No data should be collected locally or sent to external servers.
+
+## What Telemetry Does Today
+
+1. **Local collection** — Events and activity metrics stored in SQLite (`db_tevent`, `db_activity` tables)
+2. **Periodic upload** — Sent to `https://api.waveterm.dev/central` every 4 hours, on startup, on shutdown
+3. **Diagnostics ping** — Sent to `https://ping.waveterm.dev/central` once on startup, then at most once/day
+4. **Opt-out notification** — When user disables telemetry, a single record is sent to `/no-telemetry`
+5. **Electron activity tracking** — `emain/emain.ts` collects display info, foreground/active state, terminal command counts, and AI usage minutes; sends via `ActivityCommand` and `RecordTEventCommand`
+6. **wsh CLI activity** — Every `wsh` command reports its name and success/failure via `WshActivityCommand`
+
+## Scope
+
+### What to remove
+
+- All event recording (`RecordTEvent`, `GoRecordTEventWrap`)
+- All activity tracking (`UpdateActivity`, `GoUpdateActivityWrap`, `WshActivityCommand`)
+- All cloud uploads (`wcloud.SendAllTelemetry`, `wcloud.SendDiagnosticPing`, `wcloud.SendNoTelemetryUpdate`)
+- Telemetry config setting (`telemetry:enabled`)
+- Telemetry loops in `main-server.go` (including `diagnosticLoop`)
+- Telemetry RPC commands (including `WshActivityCommand`)
+- Telemetry call sites in all packages and `emain/`
+- `pkg/telemetry/` and `pkg/wcloud/` directories
+- Telemetry documentation
+- `WAVETERM_NOPING` env var handling
+- `WCLOUD_ENDPOINT` / `WCLOUD_PING_ENDPOINT` env var handling (via `wcloud.CacheAndRemoveEnvVars`)
+- Onboarding telemetry consent flow
+- Electron activity tracking (`emain-activity.ts` increment functions, IPC handler)
+- `telemetryrequired.tsx` (telemetry consent gate for AI panel)
+
+### What to keep
+
+- `wstore.GetClientId()` / `SetClientId()` — ClientId is used for non-telemetry purposes (durable sessions, remote SSH, job manager)
+- `pkg/waveobj/wtype.go` `TosAgreed` field — Keep for now; removing it requires DB migration schema changes. `AgreeTos()` in `clientservice.go` stays since it's a TOS acceptance, not telemetry
+- `autoupdate:*` settings — `AutoUpdateEnabled`, `AutoUpdateChannel`, etc. are genuine auto-update config, not telemetry. Keep all `autoupdate` fields in `SettingsConfig` and `metaconsts.go`
+
+### Auto-generated files warning
+
+The following TypeScript files are **auto-generated** by `cmd/generatets/main-generatets.go` and **must not be edited manually** — changes will be overwritten on rebuild:
+
+- `frontend/types/gotypes.d.ts` (contains `ActivityUpdate`, `TEvent`, `TEventProps`, `TEventUserProps`, `telemetry:enabled` type)
+- `frontend/app/store/services.ts` (contains `TelemetryUpdate()` method)
+- `frontend/app/store/wshclientapi.ts` (contains `ActivityCommand()`, `RecordTEventCommand()`, `SendTelemetryCommand()`, `WaveAIEnableTelemetryCommand()`, `WshActivityCommand()`)
+
+Instead, remove the Go source types/methods that feed the generator (Phase A), then regenerate. After regeneration, also remove manual frontend call sites that reference these methods (Phase B).
+
+Similarly, `pkg/wshrpc/wshclient/wshclient.go` is auto-generated by `cmd/generatego/main-generatego.go`. Remove the Go source types (Phase A.3), then regenerate.
+
+## Implementation Phases
+
+### Phase A: Remove call sites (make everything no-op)
+
+**Goal:** Every telemetry call site is removed. The app builds and runs without collecting any data. `pkg/telemetry/` and `pkg/wcloud/` stay intact but orphaned.
+
+#### A.1: Remove telemetry from main server loops
+
+**File:** `cmd/server/main-server.go`
+
+- Remove imports: `telemetry`, `telemetrydata`, `wcloud`
+- Remove constants: `InitialTelemetryWait`, `TelemetryTick`, `TelemetryInterval`, `TelemetryInitialCountsWait`, `TelemetryCountsInterval`, `InitialDiagnosticWait`, `DiagnosticTick`
+- Remove functions: `telemetryLoop()`, `diagnosticLoop()`, `sendDiagnosticPing()`, `setupTelemetryConfigHandler()`, `panicTelemetryHandler()`, `sendTelemetryWrapper()`, `updateTelemetryCounts()`, `updateTelemetryCountsLoop()`, `beforeSendActivityUpdate()`, `startupActivityUpdate()`, `shutdownActivityUpdate()`
+- Remove from startup sequence:
+ - `sendTelemetryWrapper()` call
+ - `go telemetryLoop()`
+ - `go diagnosticLoop()`
+ - `setupTelemetryConfigHandler()`
+ - `go updateTelemetryCountsLoop()`
+ - `wcloud.CacheAndRemoveEnvVars()` call
+ - `wcloud.SendDiagnosticPing()` call
+ - `panichandler.PanicTelemetryHandler = panicTelemetryHandler`
+ - `go startupActivityUpdate(firstLaunch)` call
+ - `shutdownActivityUpdate()` call
+- Remove all `telemetry.UpdateActivity()` calls
+- Remove all `telemetry.RecordTEvent()` calls (`app:startup`, `app:shutdown`, `app:counts`, `app:display`, etc.)
+- Remove all `telemetrydata.TEventUserProps` and `TEventProps` construction
+- Remove `telemetry.AutoUpdateChannel()`, `telemetry.IsAutoUpdateEnabled()`, `telemetry.GetTosAgreedTs()` calls (these only exist in telemetry payloads; the actual `autoupdate:*` settings remain in config)
+- Remove `os.Getenv("WAVETERM_NOPING")` check and related code
+
+#### A.2: Remove telemetry from wshserver
+
+**File:** `pkg/wshrpc/wshserver/wshserver.go`
+
+- Remove imports: `telemetry`, `telemetrydata`, `wcloud`
+- Remove methods: `RecordTEventCommand()`, `SendTelemetryCommand()`, `WaveAIEnableTelemetryCommand()`, `ActivityCommand()`, **`WshActivityCommand()`**
+- Remove telemetry calls in `WshRunCommand()` (the `activityUpdate` and `GoRecordTEventWrap` at lines ~1338-1344)
+
+#### A.3: Remove telemetry RPC types
+
+**File:** `pkg/wshrpc/wshrpctypes.go`
+
+- Remove from interface: `ActivityCommand()`, `RecordTEventCommand()`, `SendTelemetryCommand()`, `WaveAIEnableTelemetryCommand()`, **`WshActivityCommand()`**
+- Remove type: `ActivityUpdate` struct
+- Remove import: `telemetrydata` (if no other uses)
+
+#### A.4: Remove telemetry RPC client helpers
+
+**File:** `pkg/wshrpc/wshclient/wshclient.go` (auto-generated)
+
+- Remove: `ActivityCommand()`, `RecordTEventCommand()`, `SendTelemetryCommand()`, `WaveAIEnableTelemetryCommand()`, **`WshActivityCommand()`**
+- Remove import: `telemetrydata` (if no other uses)
+- **Note:** This file is auto-generated by `cmd/generatego/main-generatego.go`. Remove the source types in A.3, then regenerate. Do NOT edit `wshclient.go` directly.
+
+#### A.5: Remove telemetry from block creation
+
+**File:** `pkg/wcore/block.go`
+
+- Remove imports: `telemetry`, `telemetrydata`
+- Remove: `recordBlockCreationTelemetry()` function
+- Remove calls to `recordBlockCreationTelemetry()` in `CreateBlock()` and `CreateBlockWithTelemetry()`
+- Rename `CreateBlockWithTelemetry` to `CreateBlock` (or keep name, just remove the telemetry parameter)
+
+#### A.6: Remove telemetry from workspace
+
+**File:** `pkg/wcore/workspace.go`
+
+- Remove imports: `telemetry`, `telemetrydata`
+- Remove: `GoUpdateActivityWrap()` and `GoRecordTEventWrap()` calls
+
+#### A.7: Remove telemetry from wcore
+
+**File:** `pkg/wcore/wcore.go`
+
+- Remove import: `wcloud`
+- Remove: `GoSendNoTelemetryUpdate()` function
+
+#### A.8: Remove telemetry from connection controller
+
+**File:** `pkg/remote/conncontroller/conncontroller.go`
+
+- Remove imports: `telemetry`, `telemetrydata`
+- Remove all `GoUpdateActivityWrap()` and `GoRecordTEventWrap()` calls (lines ~760, 763, 781, 784, 987)
+
+#### A.9: Remove telemetry from WSL connection
+
+**File:** `pkg/wslconn/wslconn.go`
+
+- Remove imports: `telemetry`, `telemetrydata`
+- Remove all `GoUpdateActivityWrap()` and `GoRecordTEventWrap()` calls (lines ~515, 518, 531, 534)
+
+#### A.10: Remove telemetry from job controller
+
+**File:** `pkg/jobcontroller/jobcontroller.go`
+
+- Remove imports: `telemetry`, `telemetrydata`
+- Remove all `GoRecordTEventWrap()` calls (lines ~730, 757, 1040, 1135, 1160)
+
+#### A.11: Remove telemetry from AI usechat
+
+**File:** `pkg/aiusechat/usechat.go`
+
+- Remove imports: `telemetry`, `telemetrydata`
+- Remove: `sendAIMetricsTelemetry()` function
+- Remove the `telemetry.IsTelemetryEnabled()` check for cloud modes (line ~86) — either remove the gate or hardcode it to pass
+
+#### A.12: Remove telemetry from panic handler
+
+**File:** `pkg/panichandler/panichandler.go`
+
+- Remove: `PanicTelemetryHandler` variable
+- Remove the `if PanicTelemetryHandler != nil` block from `PanicHandler`
+- Delete `PanicHandlerNoTelemetry` function entirely (it becomes redundant)
+- **Do NOT rename** `PanicHandlerNoTelemetry` to `PanicHandler` — instead, keep `PanicHandler` as the name and strip the telemetry dispatch from it. After this change, `PanicHandler` behaves exactly as `PanicHandlerNoTelemetry` did.
+- No callers outside `pkg/telemetry/` need renaming (the only callers of `PanicHandlerNoTelemetry` are in `telemetry.go`, which gets deleted in Phase C)
+- Remove `panichandler.PanicTelemetryHandler = panicTelemetryHandler` from `main-server.go` (already listed in A.1)
+
+#### A.13: Remove telemetry from client service
+
+**File:** `pkg/service/clientservice/clientservice.go`
+
+- Remove: `TelemetryUpdate()` method
+- Keep: `AgreeTos()` method (TOS acceptance, not telemetry)
+
+#### A.14: Remove telemetry config fields
+
+**File:** `pkg/wconfig/settingsconfig.go`
+
+- Remove from `SettingsConfig`: `TelemetryClear`, `TelemetryEnabled`
+- Update `CountCustomSettings()`: Remove the `telemetry:enabled` exclusion check (line ~993). Keep the `autoupdate:channel` exclusion since that's not telemetry.
+
+**File:** `pkg/wconfig/metaconsts.go`
+
+- Remove: `ConfigKey_TelemetryClear`, `ConfigKey_TelemetryEnabled`
+- **Note:** This file is auto-generated by `cmd/generatego/main-generatego.go` via `GenerateSettingsMetaConsts()`. After removing the fields from `SettingsConfig`, regenerate.
+
+#### A.15: Remove debug CLI command
+
+**File:** `cmd/wsh/cmd/wshcmd-debug.go`
+
+- Remove: `debugSendTelemetryCmd` and `debugSendTelemetryRun()`
+
+#### A.16: Remove wsh CLI activity tracking
+
+**File:** `cmd/wsh/cmd/wshcmd-root.go`
+
+- Remove: `sendActivity()` function (lines 221-231)
+- Remove: `activityWrap()` function (lines 106-113)
+- Remove the comment block above `sendActivity` (lines 216-218)
+- Remove `wshclient` import if no other uses remain in the file
+- Update all command `RunE` assignments that use `activityWrap` to use the inner function directly
+
+**File:** `cmd/wsh/cmd/wshcmd-file.go`
+
+- Change all `activityWrap("file", )` to just `` in `RunE` assignments:
+ - `activityWrap("file", fileListRun)` → `fileListRun`
+ - `activityWrap("file", fileCatRun)` → `fileCatRun`
+ - `activityWrap("file", fileInfoRun)` → `fileInfoRun`
+ - `activityWrap("file", fileRmRun)` → `fileRmRun`
+ - `activityWrap("file", fileWriteRun)` → `fileWriteRun`
+ - `activityWrap("file", fileAppendRun)` → `fileAppendRun`
+ - `activityWrap("file", fileCpRun)` → `fileCpRun`
+ - `activityWrap("file", fileMvRun)` → `fileMvRun`
+
+#### A.17: Remove code generator telemetrydata import
+
+**File:** `cmd/generatego/main-generatego.go`
+
+- Remove `"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"` from the imports list in `GenerateWshClient()` (line 29)
+- After Phase A.3 removes the telemetry RPC types from `wshrpctypes.go`, regenerate `wshclient.go`
+
+**File:** `cmd/generatets/main-generatets.go`
+
+- No direct changes needed — it reads Go type information via reflection. After Phase A removes the telemetry types from Go sources, regeneration will automatically exclude them.
+
+### Phase B: Remove frontend telemetry
+
+**Goal:** No telemetry calls from the frontend. No telemetry in onboarding.
+
+#### B.1: Remove recordTEvent from global store
+
+**File:** `frontend/app/store/global.ts`
+
+- Remove: `recordTEvent()` function
+- Remove from exports
+
+#### B.2: Remove telemetry RPC client methods
+
+**File:** `frontend/app/store/wshclientapi.ts` (auto-generated)
+
+- Remove: `ActivityCommand()`, `RecordTEventCommand()`, `SendTelemetryCommand()`, `WaveAIEnableTelemetryCommand()`, `WshActivityCommand()`
+- **Note:** This file is auto-generated by `cmd/generatets/main-generatets.go`. After removing the Go source types in A.3, regenerate. Then remove manual frontend call sites (B.4).
+
+#### B.3: Remove TelemetryUpdate from services
+
+**File:** `frontend/app/store/services.ts` (auto-generated)
+
+- Remove: `TelemetryUpdate()` method from `ClientService`
+- **Note:** This file is auto-generated. After removing `TelemetryUpdate()` from `clientservice.go` (A.13), regeneration will exclude it. Then remove manual frontend call sites (B.5).
+
+#### B.4: Remove recordTEvent call sites (non-AI)
+
+| File | What to remove |
+|------|----------------|
+| `frontend/app/block/blockframe-header.tsx` | `recordTEvent("action:magnify")`, `ActivityCommand({nummagnify: 1})` |
+| `frontend/app/block/connectionbutton.tsx` | `recordTEvent("action:other")` |
+| `frontend/app/block/durable-session-flyover.tsx` | `recordTEvent("action:termdurable")` |
+| `frontend/app/tab/tabcontextmenu.ts` | `recordTEvent("action:settabtheme")`, `ActivityCommand({settabtheme: 1})` |
+| `frontend/app/view/term/osc-handlers.ts` | All `recordTEvent` calls (`conn:connect`, `action:term`); also remove `incrementTermCommands` IPC call |
+| `frontend/app/view/term/term-model.ts` | `recordTEvent("action:term")` |
+| `frontend/app/view/waveconfig/waveconfig-model.ts` | `RecordTEventCommand` calls |
+| `frontend/app/workspace/workspace-layout-model.ts` | `recordTEvent("action:openwaveai")` |
+| `frontend/app/modals/about.tsx` | `RecordTEventCommand` call |
+| `frontend/app/store/keymodel.ts` | `recordTEvent` import and call (line 653: `"action:other"` for conndropdown) |
+| `frontend/app/block/blockenv.ts` | `ActivityCommand` type reference |
+| `frontend/app/tab/tabbarenv.ts` | `ActivityCommand` type reference |
+| `frontend/app/tab/tab.tsx` | `ActivityCommand` type reference |
+| `frontend/app/tab/vtabbarenv.ts` | `ActivityCommand` type reference |
+| `frontend/app/view/waveconfig/waveconfigenv.ts` | `RecordTEventCommand` type reference |
+
+#### B.5: Remove telemetry from onboarding
+
+This section requires UI restructuring, not just deletion of telemetry calls. The current onboarding flow is:
+
+```
+init (telemetry toggle + TOS) → (enabled → features) | (disabled → notelemetrystar → features)
+```
+
+After telemetry removal, the flow should be:
+
+```
+init (TOS only) → features
+```
+
+**File:** `frontend/app/onboarding/onboarding.tsx`
+
+- Remove `InitPage`'s `telemetryUpdateFn` prop
+- Remove `telemetry:enabled` setting read (`useSettingsKeyAtom("telemetry:enabled")`)
+- Remove `telemetryEnabled` / `setTelemetryEnabled` state
+- Remove `setTelemetry` function
+- Remove the telemetry toggle checkbox UI (the entire "Anonymous usage data" section)
+- Remove `telemetryEnabled` check in `acceptTos` — if AI panel is still present, always open it (or let AI removal spec handle this)
+- Remove `NoTelemetryStarPage` component entirely
+- Remove `"notelemetrystar"` page state from `pageNameAtom`
+- Simplify page flow: `acceptTos` always goes to `"features"`
+- Remove `RecordTEventCommand` calls in `handleStarClick` and `handleMaybeLater`
+- Remove `TelemetryUpdate` import/call (via `services.ClientService.TelemetryUpdate`)
+- Keep `AgreeTos` call in `acceptTos` (TOS acceptance is not telemetry)
+- Keep `RecordTEventCommand` calls in `handleStarClick`/`handleMaybeLater` — remove them. The `SetMetaCommand` call (for `"onboarding:githubstar"`) should stay since it's local metadata, not telemetry upload.
+- Update `NewInstallOnboardingModal`: remove `telemetryUpdateFn` prop from `InitPage` instantiation
+- Move the GitHub star prompt from `NoTelemetryStarPage` to `InitPage` or `FeaturesPage` (without the "telemetry disabled" framing)
+
+**Other onboarding files:**
+
+| File | What to remove |
+|------|----------------|
+| `frontend/app/onboarding/onboarding-starask.tsx` | All `RecordTEventCommand` calls |
+| `frontend/app/onboarding/onboarding-features.tsx` | All `RecordTEventCommand` calls |
+| `frontend/app/onboarding/onboarding-durable.tsx` | `RecordTEventCommand` call |
+| `frontend/app/onboarding/onboarding-upgrade-minor.tsx` | All `RecordTEventCommand` calls |
+| `frontend/app/onboarding/onboarding-upgrade-v0131.tsx` | `RecordTEventCommand` calls (if any) |
+
+**Preview files:**
+
+| File | What to remove |
+|------|----------------|
+| `frontend/preview/previews/onboarding.preview.tsx` | Remove `telemetryUpdateFn` prop from `InitPage` (after onboarding restructuring) |
+| `frontend/preview/mock/mockfilesystem.ts` | Remove `telemetry.log` mock entry (line 317) |
+
+> Note: Onboarding files that are part of the AI removal spec (`.pi/specs/remove-waveai.md`) can have their telemetry removed as part of that work instead.
+
+#### B.6: Remove telemetry from AI panel files
+
+> These are handled by the AI removal spec (`.pi/specs/remove-waveai.md`). The AI panel files have extensive telemetry calls that go away when the AI panel is removed.
+
+| File | Handled by |
+|------|------------|
+| `frontend/app/aipanel/*.tsx` | remove-waveai.md Phase A |
+| `frontend/app/onboarding/fakechat.tsx` | remove-waveai.md Phase A |
+
+**Special case:** `frontend/app/aipanel/telemetryrequired.tsx` — This is a telemetry consent gate component (`TelemetryRequiredMessage`) that blocks AI panel usage until the user enables telemetry. It calls `RpcApi.WaveAIEnableTelemetryCommand`. This component is about **telemetry**, not AI. If the AI removal spec removes the AI panel entirely, this file gets deleted with it. If the AI panel is kept, this component must be removed or replaced with a non-telemetry gate. **Coordinate with AI removal spec.**
+
+**Special case:** `frontend/app/aipanel/waveai-model.tsx` and `frontend/app/aipanel/aimode.tsx` — These read `telemetry:enabled` to gate AI cloud features (returning `"invalid"` mode or blocking cloud AI when telemetry is disabled). After `telemetry:enabled` is removed from settings, these reads will return `undefined/false`, which could permanently disable AI cloud features for existing users. If the AI removal spec does not remove these files, replace the `telemetry:enabled` reads with `true` (always allow). **Coordinate with AI removal spec.**
+
+#### B.7: Remove Electron main process telemetry
+
+**File:** `emain/emain.ts`
+
+- Remove `sendDisplaysTDataEvent()` function (sends display info via `RecordTEventCommand` with `"app:display"`)
+- Remove `logActiveState()` function (the core activity tracking loop that calls `ActivityCommand` and `RecordTEventCommand` with `"app:activity"`)
+- Remove all `RpcApi.RecordTEventCommand` calls
+- Remove all `RpcApi.ActivityCommand` calls
+- Remove `ActivityUpdate`, `TEventProps`, `ActivityDisplayType` type imports
+- Remove references to `getActivityState`, `setWasActive`, `setWasInFg`, `incrementTermCommands*`, `getAndClearTermCommands*` from `emain-activity`
+
+**File:** `emain/emain-activity.ts`
+
+- Remove `incrementTermCommandsRun()`, `incrementTermCommandsRemote()`, `incrementTermCommandsWsl()`, `incrementTermCommandsDurable()` functions
+- Remove `getAndClearTermCommandsRun()`, `getAndClearTermCommandsRemote()`, `getAndClearTermCommandsWsl()`, `getAndClearTermCommandsDurable()` functions
+- Remove `termCommandsRun`, `termCommandsRemote`, `termCommandsWsl`, `termCommandsDurable` variables
+- Keep `wasActive`, `wasInFg`, `setWasActive`, `setWasInFg`, `getActivityState` if they have non-telemetry callers (verify first; if only used by `logActiveState`, remove them too)
+- Keep quit/starting/relaunching state variables (`globalIsQuitting`, `globalIsStarting`, `globalIsRelaunching`, `forceQuit`, `userConfirmedQuit`) — these are process lifecycle state, not telemetry
+
+**File:** `emain/emain-ipc.ts`
+
+- Remove `"increment-term-commands"` IPC handler (lines ~441-454)
+- Remove imports of `incrementTermCommandsDurable`, `incrementTermCommandsRemote`, `incrementTermCommandsRun`, `incrementTermCommandsWsl` from `emain-activity`
+- Keep `setWasActive` import if it's used elsewhere in `emain-ipc.ts`
+
+**File:** `emain/preload.ts`
+
+- Remove `incrementTermCommands` API exposure (line 67)
+
+**File:** `frontend/types/custom.d.ts`
+
+- Remove `incrementTermCommands` type declaration from the Electron API interface (line 131)
+
+**File:** `frontend/app/view/term/osc-handlers.ts`
+
+- Remove `getApi().incrementTermCommands({ isRemote, isWsl, isDurable })` call (line 110)
+
+#### B.8: Remove telemetryrequired.tsx (if AI panel is kept)
+
+If the AI removal spec removes the AI panel entirely, this file is deleted with it. Otherwise:
+
+**File:** `frontend/app/aipanel/telemetryrequired.tsx`
+
+- Delete the entire file
+- Remove all imports/references to `TelemetryRequiredMessage` from AI panel components
+- Remove the `telemetry:enabled` gate that renders this component — AI cloud features should work without requiring telemetry
+
+### Phase C: Delete unused packages and regenerate
+
+**Goal:** Remove `pkg/telemetry/`, `pkg/telemetry/telemetrydata/`, `pkg/wcloud/` entirely. Regenerate auto-generated files.
+
+#### C.1: Delete pkg/telemetry/
+
+- Delete entire directory (`telemetry.go`, `telemetrydata/telemetrydata.go`)
+- **Before deletion:** Verify `telemetry.AutoUpdateChannel()` and `telemetry.IsAutoUpdateEnabled()` have no remaining callers. These are convenience functions that just read `wconfig` settings. Their callers in `main-server.go` were removed in A.1. If any other callers exist, move the functions to `pkg/wconfig/` before deleting `pkg/telemetry/`.
+
+#### C.2: Delete pkg/wcloud/
+
+- Delete entire directory (`wcloud.go`, `wclouddata.go`)
+
+#### C.3: Clean up remaining imports and regenerate
+
+- Update `cmd/generatego/main-generatego.go`: Remove `"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"` from imports (if not already done in A.17)
+- Run `go mod tidy` and fix any remaining import errors
+- Regenerate auto-generated Go files: `pkg/wshrpc/wshclient/wshclient.go`, `pkg/wconfig/metaconsts.go`
+- Regenerate auto-generated TypeScript files: `frontend/types/gotypes.d.ts`, `frontend/app/store/services.ts`, `frontend/app/store/wshclientapi.ts`
+
+#### C.4: Environment variable cleanup
+
+After deleting `pkg/wcloud/`, the `WCLOUD_ENDPOINT` and `WCLOUD_PING_ENDPOINT` environment variables will no longer be read or unset. This is harmless for a fork that doesn't use cloud services, but note that these env vars will remain set in the waveterm process environment (previously `wcloud.CacheAndRemoveEnvVars()` unset them for security). No action needed unless the fork wants to sanitize env vars for other reasons.
+
+### Phase D: Clean up docs, schemas, and database
+
+#### D.1: Remove telemetry documentation
+
+- Delete: `docs/docs/telemetry.mdx`
+- Delete: `docs/docs/telemetry-old.mdx`
+- Audit: `docs/docs/config.mdx` — remove telemetry config references
+- Audit: `docs/docs/faq.mdx` — remove telemetry Q&A
+- Audit: `docs/docs/index.mdx` — remove telemetry mentions
+- Audit: `docs/docs/releasenotes.mdx` — remove telemetry release notes (optional, historical)
+
+#### D.2: Remove telemetry from other docs
+
+- Audit: `docs/docs/waveai.mdx`, `waveai-modes.mdx` — remove telemetry mentions (handled by AI removal spec)
+
+#### D.3: Drop telemetry database tables (optional)
+
+The `db_tevent` and `db_activity` tables were created by SQL migrations (`000003_activity.up.sql` and `000007_events.up.sql`). After Phase C, no code reads or writes these tables. They remain empty in existing databases.
+
+To fully clean up, add a new migration:
+
+```
+db/migrations-wstore/000012_drop_telemetry.up.sql:
+ DROP TABLE IF EXISTS db_tevent;
+ DROP TABLE IF EXISTS db_activity;
+
+db/migrations-wstore/000012_drop_telemetry.down.sql:
+ -- Recreating these tables is not necessary; the data is obsolete
+```
+
+(Verify the next available migration number — `000011_job.down.sql` is the latest existing.)
+
+#### D.4: Clean up existing users' `telemetry:enabled` config
+
+After removing `TelemetryEnabled` from `SettingsConfig`, existing users who have `telemetry:enabled` in their config JSON will have an unrecognized key. JSON unmarshaling with `omitempty` silently ignores unknown keys, so this is harmless. No migration needed. The `CountCustomSettings` fix in A.14 removes the code that explicitly checks for this key.
+
+## Verification Checklist
+
+After each phase:
+
+- [ ] `task dev` completes without errors
+- [ ] `task start` launches the app
+- [ ] No console errors about missing telemetry functions
+- [ ] No network requests to `api.waveterm.dev` or `ping.waveterm.dev`
+- [ ] No `db_tevent` or `db_activity` table writes
+- [ ] App functions normally (terminals, SSH, file browser, etc.)
+- [ ] Onboarding flow works without telemetry consent step (simplified `init → features` flow)
+- [ ] `wsh` CLI commands work without activity tracking
+- [ ] Electron activity tracking loop is no longer running
+- [ ] AI panel works without telemetry gate (if AI panel is still present)
+- [ ] Auto-generated files (`wshclient.go`, `gotypes.d.ts`, `services.ts`, `wshclientapi.ts`) have been regenerated and contain no telemetry types/methods
+
+## Risk Assessment
+
+| Risk | Mitigation |
+|------|------------|
+| ClientId is used by telemetry AND non-telemetry code | Keep `wstore.GetClientId()`/`SetClientId()` — only remove telemetry-specific usage |
+| `TosAgreed` field is used for telemetry cohorts | Keep the field in `waveobj.Client`; it's harmless without telemetry reading it. Keep `AgreeTos()` in `clientservice.go`. |
+| Onboarding flow assumes telemetry consent step | Restructure onboarding to remove telemetry toggle, simplify page flow to `init → features`, move GitHub star prompt out of `NoTelemetryStarPage`. See B.5. |
+| Upstream merge conflicts | Phase A (remove call sites) keeps `pkg/telemetry/` and `pkg/wcloud/` intact, minimizing conflicts. Phase C (delete packages) is deferred. |
+| `autoupdate:*` settings in `pkg/telemetry/` | `telemetry.AutoUpdateChannel()` and `telemetry.IsAutoUpdateEnabled()` are convenience functions that just read `wconfig` settings. Callers in `main-server.go` are removed in A.1. If any callers remain elsewhere, move these functions to `pkg/wconfig/` before deleting `pkg/telemetry/` in Phase C. |
+| Auto-generated files overwritten by manual edits | Do NOT manually edit `wshclient.go`, `gotypes.d.ts`, `services.ts`, or `wshclientapi.ts`. Remove Go source types first, then regenerate. |
+| AI panel `telemetry:enabled` reads become dangling | After removing the setting, reads return `undefined/false`. If AI panel is kept (not removed by AI spec), replace reads with `true`. See B.6. |
+| `PanicHandlerNoTelemetry` callers | Only called within `pkg/telemetry/telemetry.go` (2 calls). After Phase C deletes that package, no callers remain. No rename needed. See A.12. |
+| Existing `telemetry:enabled` in user configs | Silently ignored by JSON unmarshaling with `omitempty`. No migration needed. |
+| `WCLOUD_ENDPOINT`/`WCLOUD_PING_ENDPOINT` env vars | After removing `wcloud`, these env vars will stay set in the process. Harmless for a fork. See C.4. |
+
+## Interaction with AI Removal Spec
+
+The AI removal spec (`.pi/specs/remove-waveai.md`) and this spec overlap in these areas:
+
+- `WaveAIEnableTelemetryCommand` — remove in both (telemetry spec covers it)
+- AI panel telemetry calls — removed when AI panel is removed
+- `ai:apitokensecretname` — removed in AI spec, not telemetry
+- `telemetry.IsTelemetryEnabled()` check in AI cloud modes — removed in telemetry spec
+- `telemetryrequired.tsx` — telemetry consent gate for AI. If AI panel is deleted, this file is deleted with it. If AI panel is kept, remove this component and the `telemetry:enabled` gate. See B.6.
+- `waveai-model.tsx`/`aimode.tsx` `telemetry:enabled` reads — if AI panel is kept after AI spec, replace with `true`. See B.6.
+
+**Recommendation:** Execute telemetry spec first (Phase A), then AI spec. This way the AI spec doesn't need to worry about telemetry imports.
+
+## File Cross-Reference
+
+Complete list of all files that need changes, organized by phase:
+
+### Phase A (Go backend)
+
+| File | Section | What changes |
+|------|---------|--------------|
+| `cmd/server/main-server.go` | A.1 | Remove telemetry loops, functions, constants, imports |
+| `pkg/wshrpc/wshserver/wshserver.go` | A.2 | Remove 5 RPC methods + WshRunCommand telemetry |
+| `pkg/wshrpc/wshrpctypes.go` | A.3 | Remove 5 interface methods + `ActivityUpdate` type |
+| `pkg/wshrpc/wshclient/wshclient.go` | A.4 | Auto-generated; regenerate after A.3 |
+| `pkg/wcore/block.go` | A.5 | Remove `recordBlockCreationTelemetry` + rename |
+| `pkg/wcore/workspace.go` | A.6 | Remove activity/event tracking calls |
+| `pkg/wcore/wcore.go` | A.7 | Remove `GoSendNoTelemetryUpdate` |
+| `pkg/remote/conncontroller/conncontroller.go` | A.8 | Remove activity/event calls |
+| `pkg/wslconn/wslconn.go` | A.9 | Remove activity/event calls |
+| `pkg/jobcontroller/jobcontroller.go` | A.10 | Remove event tracking calls |
+| `pkg/aiusechat/usechat.go` | A.11 | Remove `sendAIMetricsTelemetry` + telemetry gate |
+| `pkg/panichandler/panichandler.go` | A.12 | Remove `PanicTelemetryHandler`, simplify `PanicHandler` |
+| `pkg/service/clientservice/clientservice.go` | A.13 | Remove `TelemetryUpdate` method |
+| `pkg/wconfig/settingsconfig.go` | A.14 | Remove `TelemetryClear`/`TelemetryEnabled` fields + `CountCustomSettings` fix |
+| `pkg/wconfig/metaconsts.go` | A.14 | Auto-generated; regenerate after settingsconfig change |
+| `cmd/wsh/cmd/wshcmd-debug.go` | A.15 | Remove `debugSendTelemetryCmd` |
+| `cmd/wsh/cmd/wshcmd-root.go` | A.16 | Remove `sendActivity`, `activityWrap`, comment block |
+| `cmd/wsh/cmd/wshcmd-file.go` | A.16 | Unwrap `activityWrap` from all `RunE` assignments |
+| `cmd/generatego/main-generatego.go` | A.17 | Remove `telemetrydata` import |
+
+### Phase B (Frontend + Electron)
+
+| File | Section | What changes |
+|------|---------|--------------|
+| `frontend/app/store/global.ts` | B.1 | Remove `recordTEvent` |
+| `frontend/app/store/wshclientapi.ts` | B.2 | Auto-generated; regenerate after A.3 |
+| `frontend/app/store/services.ts` | B.3 | Auto-generated; regenerate after A.13 |
+| `frontend/app/block/blockframe-header.tsx` | B.4 | Remove `recordTEvent` + `ActivityCommand` calls |
+| `frontend/app/block/connectionbutton.tsx` | B.4 | Remove `recordTEvent` call |
+| `frontend/app/block/durable-session-flyover.tsx` | B.4 | Remove `recordTEvent` call |
+| `frontend/app/tab/tabcontextmenu.ts` | B.4 | Remove `recordTEvent` + `ActivityCommand` calls |
+| `frontend/app/view/term/osc-handlers.ts` | B.4 | Remove `recordTEvent` calls + `incrementTermCommands` IPC |
+| `frontend/app/view/term/term-model.ts` | B.4 | Remove `recordTEvent` call |
+| `frontend/app/view/waveconfig/waveconfig-model.ts` | B.4 | Remove `RecordTEventCommand` calls |
+| `frontend/app/workspace/workspace-layout-model.ts` | B.4 | Remove `recordTEvent` call |
+| `frontend/app/modals/about.tsx` | B.4 | Remove `RecordTEventCommand` call |
+| `frontend/app/store/keymodel.ts` | B.4 | Remove `recordTEvent` import + call |
+| `frontend/app/block/blockenv.ts` | B.4 | Remove `ActivityCommand` type ref |
+| `frontend/app/tab/tabbarenv.ts` | B.4 | Remove `ActivityCommand` type ref |
+| `frontend/app/tab/tab.tsx` | B.4 | Remove `ActivityCommand` type ref |
+| `frontend/app/tab/vtabbarenv.ts` | B.4 | Remove `ActivityCommand` type ref |
+| `frontend/app/view/waveconfig/waveconfigenv.ts` | B.4 | Remove `RecordTEventCommand` type ref |
+| `frontend/app/onboarding/onboarding.tsx` | B.5 | Restructure: remove toggle, simplify flow |
+| `frontend/app/onboarding/onboarding-starask.tsx` | B.5 | Remove `RecordTEventCommand` calls |
+| `frontend/app/onboarding/onboarding-features.tsx` | B.5 | Remove `RecordTEventCommand` calls |
+| `frontend/app/onboarding/onboarding-durable.tsx` | B.5 | Remove `RecordTEventCommand` call |
+| `frontend/app/onboarding/onboarding-upgrade-minor.tsx` | B.5 | Remove `RecordTEventCommand` calls |
+| `frontend/app/onboarding/onboarding-upgrade-v0131.tsx` | B.5 | Remove `RecordTEventCommand` calls |
+| `frontend/preview/previews/onboarding.preview.tsx` | B.5 | Remove `telemetryUpdateFn` prop |
+| `frontend/preview/mock/mockfilesystem.ts` | B.5 | Remove `telemetry.log` mock entry |
+| `frontend/app/aipanel/telemetryrequired.tsx` | B.6/B.8 | Delete or replace (coordinate with AI spec) |
+| `emain/emain.ts` | B.7 | Remove `sendDisplaysTDataEvent`, `logActiveState`, all RPC calls |
+| `emain/emain-activity.ts` | B.7 | Remove term command tracking functions |
+| `emain/emain-ipc.ts` | B.7 | Remove `"increment-term-commands"` IPC handler |
+| `emain/preload.ts` | B.7 | Remove `incrementTermCommands` API |
+| `frontend/types/custom.d.ts` | B.7 | Remove `incrementTermCommands` type decl |
+| `frontend/types/gotypes.d.ts` | — | Auto-generated; regen removes `ActivityUpdate`, `TEvent`, `telemetry:enabled` types |
+
+### Phase C (Delete packages)
+
+| File/Directory | Section | What changes |
+|------|---------|--------------|
+| `pkg/telemetry/` | C.1 | Delete entire directory |
+| `pkg/wcloud/` | C.2 | Delete entire directory |
+| Various Go files | C.3 | `go mod tidy`, fix imports, regenerate |
+
+### Phase D (Docs + DB)
+
+| File/Directory | Section | What changes |
+|------|---------|--------------|
+| `docs/docs/telemetry.mdx` | D.1 | Delete |
+| `docs/docs/telemetry-old.mdx` | D.1 | Delete |
+| `docs/docs/config.mdx` | D.1 | Remove telemetry config refs |
+| `docs/docs/faq.mdx` | D.1 | Remove telemetry Q&A |
+| `docs/docs/index.mdx` | D.1 | Remove telemetry mentions |
+| `db/migrations-wstore/` | D.3 | Add migration to drop `db_tevent`/`db_activity` tables |
\ No newline at end of file
diff --git a/.pi/specs/remove-waveai.md b/.pi/specs/remove-waveai.md
new file mode 100644
index 0000000000..60ae210435
--- /dev/null
+++ b/.pi/specs/remove-waveai.md
@@ -0,0 +1,460 @@
+# Spec: Remove Wave AI Features
+
+**Date:** 2026-05-12
+**Status:** Draft
+
+## Goal
+
+Disable and hide all Wave AI features from the UI. Do not delete code initially — comment out or guard behind no-ops so the fork stays close to upstream and re-enabling is trivial.
+
+## Scope
+
+### What to remove/disable
+
+- Wave AI chat panel (`waveai` block type)
+- AI file diff viewer (`aifilediff` block type)
+- AI modes configuration (`waveai.json`)
+- AI presets configuration (`aipresets.json`)
+- AI-related keyboard shortcuts
+- AI focus management
+- AI RPC commands (frontend client + backend server)
+- AI web endpoints
+- AI activity telemetry
+- AI config fields in `settingsconfig.go`
+- AI documentation pages
+- `ai:apitokensecretname` field (AI token via secrets)
+- AI button in tab bar (`WaveAIButton`)
+- AI panel from workspace layout
+- AI onboarding page (`WaveAIPage`, `fakechat.tsx`)
+
+### What to keep
+
+- `pkg/secretstore/` — general encrypted key-value store (used by SSH passwords, potentially future features)
+- `ssh:passwordsecretname` — SSH password via secrets (non-AI use case)
+
+## Implementation Phases
+
+### Phase A: Disable the UI (frontend only)
+
+**Goal:** AI panels cannot be opened, AI is not visible in any menus or settings. App builds and runs without errors.
+
+#### A.1: Unregister AI block types
+
+**File:** `frontend/app/block/blockregistry.ts`
+
+- Comment out or remove: `BlockRegistry.set("waveai", WaveAiModel)`
+- Comment out or remove: `BlockRegistry.set("aifilediff", AiFileDiffViewModel)`
+- Remove imports: `WaveAiModel`, `AiFileDiffViewModel`
+
+**Verification:** App starts without errors. No AI block types registered.
+
+#### A.2: Strip AI from block utilities
+
+**File:** `frontend/app/block/blockutil.tsx`
+
+- Remove the `view == "waveai"` cases in `getBlockTitle()` and `getBlockIcon()` (or return empty/nil)
+
+#### A.3: Remove AI keyboard shortcuts
+
+**File:** `frontend/app/store/keymodel.ts`
+
+- Remove the `WaveAIModel` import
+- Remove all `WaveAIModel.getInstance()` calls (lines ~151, 155, 177, 184, 192, 199, 227, 248, 252, 260, 265, 268, 687, 691, 696, 700)
+- Remove `focusType === "waveai"` branches
+- Remove `inWaveAI` variable and related navigation logic
+
+#### A.4: Remove AI focus management
+
+**File:** `frontend/app/store/focusManager.ts`
+
+- Remove `waveAIHasFocusWithin` and `WaveAIModel` imports
+- Change `FocusStrType` from `"node" | "waveai"` to just `"node"`
+- Remove `setWaveAIFocused()` and `requestWaveAIFocus()` methods
+- Remove `"waveai"` branches in focus handling
+
+#### A.5: Remove AI global atoms
+
+**File:** `frontend/app/store/global-atoms.ts`
+
+- Remove `waveaiModeConfigAtom`
+- Remove `ai@` preset filtering logic (line ~68)
+- Remove from exported atoms list
+
+#### A.6: Remove AI event listeners
+
+**File:** `frontend/app/store/global.ts`
+
+- Remove `waveai:modeconfig` event handler
+- Remove `waveai:ratelimit` event handler
+
+#### A.7: Remove AI RPC client methods
+
+**File:** `frontend/app/store/wshclientapi.ts`
+
+- Remove: `GetWaveAIChatCommand`, `GetWaveAIModeConfigCommand`, `GetWaveAIRateLimitCommand`, `WaveAIAddContextCommand`, `WaveAIEnableTelemetryCommand`, `WaveAIGetToolDiffCommand`, `WaveAIToolApproveCommand`
+
+**File:** `frontend/app/store/tabrpcclient.ts`
+
+- Remove `WaveAIModel` import
+- Remove `handle_waveaiaddcontext()` method
+
+#### A.8: Remove AI from term model
+
+**File:** `frontend/app/view/term/term-model.ts`
+
+- Remove `WaveAIModel` import
+- Remove the AI-related code at line ~848
+
+#### A.9: Remove AI config file handling
+
+**File:** `frontend/app/view/waveconfig/waveconfig-model.ts`
+
+- Remove the `waveai.json` config file entry (line ~84)
+- Remove `validateWaveAiJson()` function
+- Remove `aipresets.json` references (line ~122)
+
+**File:** `frontend/app/monaco/schemaendpoints.ts`
+
+- Remove `waveaiSchema` import and registration
+- Remove `aipresetsSchema` import and registration
+
+**File:** `frontend/preview/mock/defaultconfig.ts`
+
+- Remove `waveaiJson` import
+- Remove `waveai` entry from mock config
+
+#### A.10: Remove AI visual component
+
+**File:** `frontend/app/view/waveconfig/waveaivisual.tsx`
+
+- Mark as unused (can keep file but it won't be imported anywhere)
+
+#### A.11: Remove AI button from tab bar
+
+**File:** `frontend/app/tab/tabbar.tsx`
+
+- Remove `WaveAIButton` component (lines ~48-76)
+- Remove `` from render (line ~616)
+- Remove `waveAIButtonRef` usage
+- Remove `export { TabBar, WaveAIButton }` — change to `export { TabBar }`
+
+#### A.12: Remove AI from workspace
+
+**File:** `frontend/app/workspace/workspace.tsx`
+
+- Remove `import { AIPanel } from "@/app/aipanel/aipanel"`
+- Remove `getApi().setWaveAIOpen(isVisible)` call (line ~87)
+- Remove `` rendering from JSX
+
+**File:** `frontend/app/workspace/workspace-layout-model.ts`
+
+- Remove `import { WaveAIModel } from "@/app/aipanel/waveai-model"`
+- Remove `waveai:panelopen` and `waveai:panelwidth` meta key handling (lines ~93, ~133, ~137)
+- Remove `getApi().setWaveAIOpen(visible)` call (line ~397)
+- Remove `WaveAIModel.getInstance().focusInput()` call (line ~409)
+- Remove the "vtab stays constant, aipanel absorbs the change" logic (line ~230)
+
+#### A.13: Remove AI from onboarding
+
+**File:** `frontend/app/onboarding/onboarding-features.tsx`
+
+- Remove `WaveAIPage` component (lines ~22-247)
+- Remove `"waveai"` from `FeaturePageName` type
+- Change default `currentPage` from `"waveai"` to next feature (e.g., `"durable"`)
+- Remove `"waveai"` case from page navigation logic
+- Remove `handlePrev()` navigation to `"waveai"`
+
+**File:** `frontend/app/onboarding/fakechat.tsx`
+
+- Mark as unused (won't be imported after WaveAIPage is removed)
+
+#### A.14: Electron main — remove AI activity tracking
+
+**File:** `emain/emain.ts`
+
+- Already clean — no AI references found (telemetry removed in prior phase)
+
+**File:** `emain/emain-window.ts`
+
+- Remove `ipcMain.on("set-waveai-open", ...)` handler (line ~760)
+
+**File:** `emain/preload.ts`
+
+- Remove `setWaveAIOpen` from IPC exposed methods (line ~65)
+
+**File:** `emain/emain-tabview.ts`
+
+- Remove `isWaveAIOpen` field from tab view struct (line ~121)
+- Remove `this.isWaveAIOpen = false` initialization (line ~145)
+
+### Phase B: Remove backend wiring (Go)
+
+**Goal:** No AI RPC handlers, no AI config fields, no AI web endpoints. `pkg/aiusechat/` stays intact but unused.
+
+#### B.1: Remove AI RPC types
+
+**File:** `pkg/wshrpc/wshrpctypes.go`
+
+- Remove from interface: `GetWaveAIModeConfigCommand`, `WaveAIEnableTelemetryCommand`, `GetWaveAIChatCommand`, `GetWaveAIRateLimitCommand`, `WaveAIToolApproveCommand`, `WaveAIAddContextCommand`, `WaveAIGetToolDiffCommand`
+- Remove types: `CommandGetWaveAIChatData`, `CommandWaveAIToolApproveData`, `CommandWaveAIAddContextData`, `CommandWaveAIGetToolDiffData`, `CommandWaveAIGetToolDiffRtnData`
+- Remove from telemetry props: `WaveAIFgMinutes`, `WaveAIActiveMinutes`
+- Remove `uctypes` import if no longer needed
+
+#### B.2: Remove AI RPC server handlers
+
+**File:** `pkg/wshrpc/wshserver/wshserver.go`
+
+- Remove: `GetWaveAIModeConfigCommand()`, `WaveAIEnableTelemetryCommand()`, `GetWaveAIChatCommand()`, `GetWaveAIRateLimitCommand()`, `WaveAIToolApproveCommand()`, `WaveAIGetToolDiffCommand()`
+- Remove imports: `aiusechat`, `chatstore`, `uctypes` (if no other uses remain)
+
+#### B.3: Remove AI RPC client helpers
+
+**File:** `pkg/wshrpc/wshclient/wshclient.go`
+
+- Remove: `GetWaveAIChatCommand()`, `GetWaveAIModeConfigCommand()`, `GetWaveAIRateLimitCommand()`, `WaveAIAddContextCommand()`, `WaveAIEnableTelemetryCommand()`, `WaveAIGetToolDiffCommand()`, `WaveAIToolApproveCommand()`
+- Remove `uctypes` import if no longer needed
+
+#### B.4: Remove AI web endpoints
+
+**File:** `pkg/web/web.go`
+
+- Remove: `/api/post-chat-message` handler
+- Remove: `/wave/aichat` handler
+- Remove `aiusechat` import if no longer needed
+
+#### B.5: Remove AI initialization
+
+**File:** `cmd/server/main-server.go`
+
+- Remove: `aiusechat.InitAIModeConfigWatcher()` call
+- Remove `aiusechat` import if no longer needed
+
+#### B.6: Remove AI config fields
+
+**File:** `pkg/wconfig/settingsconfig.go`
+
+- Remove from `FrontendConfig`: `WaveAiShowCloudModes`, `WaveAiDefaultMode`
+- Remove from `AIProviderConfig`: `WaveAICloud`, `WaveAIPremium`
+- Remove from `FullConfig`: `WaveAIModes`
+- Remove `GetCustomAIModeConfigs()` function
+- Remove `ai:apitokensecretname` from `AIProviderConfig` (field `APITokenSecretName`)
+- Remove `AIModeConfigType` if no longer referenced
+
+#### B.7: Remove AI TypeScript generation
+
+**File:** `pkg/tsgen/tsgenevent.go`
+
+- Remove: `Event_WaveAIRateLimit` mapping
+- Remove `uctypes` import if no longer needed
+
+#### B.8: Remove default AI config
+
+**File:** `pkg/wconfig/defaultconfig/waveai.json`
+
+- Delete or mark as unused (won't be loaded if `WaveAIModes` is removed from config)
+
+### Phase C: Clean up docs & schemas
+
+**Goal:** No AI references in public-facing documentation or JSON schemas.
+
+#### C.1: Remove AI documentation
+
+- Delete: `docs/docs/waveai.mdx`
+- Delete: `docs/docs/waveai-modes.mdx`
+- Delete: `docs/docs/ai-presets.mdx`
+- Audit: `docs/docs/secrets.mdx` — remove AI token examples, keep SSH password secret examples
+- Audit: `docs/docs/config.mdx` — remove AI config references
+- Audit: `docs/docs/telemetry.mdx` — remove AI telemetry references
+- Audit: `docs/docs/connections.mdx` — remove `ai:apitokensecretname` references
+
+#### C.2: Remove JSON schemas
+
+- Delete: `schema/waveai.json`
+- Delete: `schema/aipresets.json`
+
+### Phase D: Delete unused code (optional, later)
+
+**Goal:** Remove dead code after the fork is stable and verified.
+
+- Delete: `pkg/aiusechat/` (entire directory, ~12K lines)
+- Delete: `frontend/app/aipanel/` (17 files)
+- Delete: `frontend/app/view/waveai/waveai.tsx`
+- Delete: `frontend/app/view/aifilediff/aifilediff.tsx`
+- Delete: `frontend/app/view/waveconfig/waveaivisual.tsx`
+
+## Implementation Order
+
+Start with deepest dependencies and work up to UI components to avoid dangling imports:
+
+1. **A.1–A.2** — Block registry + utilities (foundation)
+2. **A.3–A.6** — Store layer (keyboard, focus, atoms, events)
+3. **A.7–A.9** — RPC clients + config handling
+4. **A.10** — Visual component (orphaned)
+5. **A.11–A.13** — UI components (tab bar, workspace, onboarding)
+6. **A.14** — Electron main (IPC cleanup)
+
+## Verification Checklist
+
+After each phase:
+
+- [ ] `task dev` completes without errors
+- [ ] `task start` launches the app
+- [ ] No console errors related to missing AI components
+- [ ] No AI panels appear in the UI
+- [ ] No AI entries in settings/config UI
+- [ ] No AI keyboard shortcuts active
+- [ ] App functions normally for non-AI features (terminals, file browser, SSH connections)
+
+## Phase A Review — 2026-05-14
+
+### Issues Found During Review
+
+#### 🔴 A.15: Builder workspace still imports AIPanel and WaveAIModel (Not in original spec)
+
+The builder subsystem (`frontend/builder/`) has deep AI integration that was not covered by the original Phase A spec. These are live imports that will crash if `aipanel/` is ever deleted (Phase D).
+
+**Files:**
+- `frontend/builder/builder-workspace.tsx` — imports `AIPanel` from `@/app/aipanel/aipanel`, renders ``
+- `frontend/builder/builder-buildpanel.tsx` — imports `WaveAIModel`, calls `WaveAIModel.getInstance()` for "Add to Context" context menu and AI model access
+- `frontend/builder/tabs/builder-previewtab.tsx` — imports `WaveAIModel`, calls `WaveAIModel.getInstance()` for chat ID access
+- `frontend/builder/tabs/builder-filestab.tsx` — imports `formatFileSize` from `@/app/aipanel/ai-utils` (generic utility trapped in AI module)
+- `frontend/builder/store/builder-focusmanager.ts` — has `BuilderFocusType = "waveai" | "app"` and `setWaveAIFocused()` method
+
+**Fix:** Remove AIPanel from builder workspace, replace WaveAIModel calls with stubs or no-ops, move `formatFileSize` to a shared utility, change `BuilderFocusType` to just `"app"`.
+
+#### 🟡 A.9 partial: "AI Presets" deprecated config entry still in settings UI
+
+`frontend/app/view/waveconfig/waveconfig-model.ts` still has:
+- `validateAiJson()` function (lines 35-44) that validates keys starting with `ai@`
+- "AI Presets" deprecated config file entry (lines 93-103) pointing to `presets/ai.json` with `docsUrl: "https://docs.waveterm.dev/ai-presets"`
+
+This means AI Presets still appears in the settings UI as a deprecated file.
+
+**Fix:** Remove `validateAiJson()` and the "AI Presets" entry from `deprecatedConfigFiles`.
+
+#### 🟡 A.3 partial: `inWaveAI` dead code in layoutModel.ts
+
+`frontend/layout/lib/layoutModel.ts` line 1107 still has `inWaveAI` parameter in `switchNodeFocusInDirection()`, and lines 1127-1131 have WaveAI-specific navigation logic. Caller in `keymodel.ts` passes `false`, so it's harmless but is dead code.
+
+**Fix:** Remove `inWaveAI` parameter and the WaveAI-specific branch from `switchNodeFocusInDirection()`. Update caller in `keymodel.ts`.
+
+#### 🟢 A.14 partial: Mock Electron API still has `setWaveAIOpen`
+
+`frontend/preview/mock/preview-electron-api.ts` line 53 still has `setWaveAIOpen: (_isOpen: boolean) => {}`.
+
+**Fix:** Remove `setWaveAIOpen` from the mock API object.
+
+#### 🟢 Dead `rateLimitInfoAtom` declaration in global-atoms.ts
+
+`frontend/app/store/global-atoms.ts` line 115 declares `rateLimitInfoAtom` but never exports it or adds it to the `atoms` object. Leftover from AI rate limit tracking.
+
+**Fix:** Remove the `rateLimitInfoAtom` declaration.
+
+### Deferred Items (Not Phase A Bugs)
+
+These are expected to be cleaned up in later phases:
+
+| Item | Why Deferred | Phase |
+|------|-------------|-------|
+| Auto-generated TS types (`gotypes.d.ts`, `waveevent.d.ts`, `wshclientapi.ts`, `custom.d.ts`) still have AI definitions | Regeneration depends on Go backend types being removed first | B → regenerate |
+| `wshclientapi.ts` still has 7 AI RPC commands + `AiSendMessageCommand` | Auto-generated file; will be regenerated after Go types removed | B → regenerate |
+| `aipanel/`, `aifilediff/`, `fakechat.tsx`, `waveaivisual.tsx` files still exist | Intentionally kept per spec; Phase D deletes them | D |
+| `waveai.tsx` stub still exports `WaveAiModel` class | File exists but not registered in blockregistry; dead code | D |
+| `schema/waveai.json`, `schema/aipresets.json` still exist | Phase C removes them | C |
+| `schema/settings.json` has `waveai:showcloudmodes` and `waveai:defaultmode` | Phase B/C handles schema cleanup | B/C |
+| `docs/docs/waveai.mdx`, `waveai-modes.mdx`, `ai-presets.mdx` still exist | Phase C removes them | C |
+| `docs/docs/config.mdx` still has `waveai:showcloudmodes`/`waveai:defaultmode` | Phase C audits this | C |
+| `pkg/wconfig/defaultconfig/waveai.json`, `presets/ai.json` still exist | Phase B.8 handles this | B |
+| All Go backend types and handlers untouched | Phase B scope | B |
+| `filebackup.go` uses `waveai-backups` directory name | Low priority; just a directory name, harmless | D |
+
+### Unintended Consequences to Track
+
+1. **Builder mode will break at Phase D** — The builder imports `AIPanel`, `WaveAIModel`, and `formatFileSize` from `aipanel/`. When that directory is deleted in Phase D, the builder crashes unless A.15 fixes are applied first.
+2. **`formatFileSize` trapped in AI module** — `builder-filestab.tsx` imports it from `@/app/aipanel/ai-utils`. Must be relocated before Phase D.
+3. **Type definitions out of sync** — Auto-generated TS files have AI types that no longer match runtime reality. No runtime errors but creates confusion. Will resolve when Go types removed and generator re-run.
+
+## Phase B Review — 2026-05-15
+
+### Items Already Removed (from telemetry phase)
+
+- `WaveAIEnableTelemetryCommand` — already gone from interface, server, and client
+- `WaveAIFgMinutes` / `WaveAIActiveMinutes` telemetry props — already gone from `wshrpctypes.go`
+- `GetCustomAIModeConfigs()` — spec mentioned it but it doesn't exist in `settingsconfig.go` (it's `ComputeResolvedAIModeConfigs()` in `aiusechat/`)
+
+### Additional Items Found (Not in Original Spec)
+
+| ID | File | What to remove |
+|----|------|---------------|
+| **B.1 (extra)** | `pkg/wshrpc/wshrpctypes.go` | `AiSendMessageCommand` interface method + `AiMessageData` type (no server handler exists) |
+| **B.3 (extra)** | `pkg/wshrpc/wshclient/wshclient.go` | `AiSendMessageCommand()` helper function |
+| **B.6 (extra)** | `pkg/wconfig/settingsconfig.go` | `CountCustomAIModes()` function (dead code) |
+| **B.9** | `pkg/wps/wpstypes.go` | `Event_WaveAIRateLimit`, `Event_AIModeConfig` constants + from `AllEvents` list |
+| **B.10** | `pkg/wconfig/metaconsts.go` | `ConfigKey_WaveAiShowCloudModes`, `ConfigKey_WaveAiDefaultMode` (auto-generated; will not reappear after B.6) |
+| **B.11** | `pkg/tsgen/tsgen.go` | `uctypes.RateLimitInfo{}` and `wconfig.AIModeConfigUpdate{}` from `Types` slice, `aiusechat/uctypes` import |
+| **B.12** | `cmd/generateschema/main-generateschema.go` | `WaveSchemaWaveAIFileName` const + waveai schema generation block |
+| **B.13** | `cmd/generatego/main-generatego.go` | `"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"` from boilerplate import list |
+
+### Updated Implementation Order
+
+1. **B.1** — `wshrpctypes.go` (interface + types, including `AiSendMessageCommand` + `AiMessageData`)
+2. **B.2** — `wshserver.go` (server handlers)
+3. **B.3** — `wshclient.go` (client helpers, including `AiSendMessageCommand`)
+4. **B.4** — `web.go` (web endpoints)
+5. **B.5** — `main-server.go` (init call)
+6. **B.6** — `settingsconfig.go` (config types + `CountCustomAIModes`)
+7. **B.7** — `tsgenevent.go` (event type mapping)
+8. **B.11** — `tsgen.go` (Types slice + import)
+9. **B.9** — `wpstypes.go` (event constants + AllEvents list)
+10. **B.13** — `generatego/main-generatego.go` (import list)
+11. **B.12** — `generateschema/main-generateschema.go` (schema generation)
+12. **B.10** — `metaconsts.go` (AI config key constants)
+13. **B.8** — `defaultconfig/waveai.json` (delete)
+
+### Left Untouched
+
+- `cmd/testai/`, `cmd/testopenai/`, `cmd/testsummarize/` — test-only binaries; harmless dead code
+- `pkg/aiusechat/` — entire package stays intact (Phase D)
+
+### Phase B Completion — 2026-05-15
+
+All Phase B items implemented and verified. `task build:backend` completes without errors.
+
+**Additional item found during implementation:**
+- `cmd/wsh/cmd/wshcmd-ai.go` — `wsh ai` CLI command, deleted (used `AIAttachedFile`, `CommandWaveAIAddContextData`, `WaveAIAddContextCommand`)
+
+**Post-Phase B state:**
+- `pkg/aiusechat/` is now a dead package (no external callers)
+- Auto-generated TS types (`gotypes.d.ts`, `waveevent.d.ts`, `wshclientapi.ts`) still have stale AI definitions — will be regenerated when the generator is re-run
+- `schema/waveai.json` still exists on disk (was pre-generated, not regenerated by `task build:schema` since the generator code is removed) — Phase C will delete it
+
+## Phase C Review — 2026-05-16
+
+### Completed
+
+- **C.1**: Deleted `docs/docs/waveai.mdx`, `waveai-modes.mdx`, `ai-presets.mdx`
+- **C.1**: Cleaned `docs/docs/config.mdx` — removed 13 AI config rows (`ai:*`, `waveai:*`, `app:hideaibutton`), cleaned default config JSON, updated env var examples
+- **C.1**: Cleaned `docs/docs/gettingstarted.mdx` — removed AI mentions from intro, key features, quick start, and next steps
+- **C.1**: Cleaned `docs/docs/index.mdx` — removed Wave AI card, removed AI from intro text
+- **C.1**: Cleaned `docs/docs/wsh-reference.mdx` — removed `waveai` from view type filter, removed `presets/ai.json` example
+- **C.1**: Truncated `docs/docs/releasenotes.mdx` — kept v0.14.x only (v0.13.1 and earlier removed), stripped all AI mentions from v0.14.x entries
+- **C.2**: Deleted `schema/waveai.json`, `schema/aipresets.json`
+- **Phase A carryover**: Fixed misleading AI text in `frontend/builder/tabs/builder-previewtab.tsx` EmptyStateView
+
+### Audit Results
+
+| File | AI references found | Action |
+|------|-------------------|--------|
+| `docs/docs/secrets.mdx` | None (already clean) | No change needed |
+| `docs/docs/telemetry.mdx` | File doesn't exist (removed in telemetry phase) | N/A |
+| `docs/docs/connections.mdx` | None | No change needed |
+
+## Risk Assessment
+
+| Risk | Mitigation |
+|------|------------|
+| AI code is imported by non-AI files | Phase A handles frontend imports first; Phase B handles Go imports. Each phase is independently buildable. Builder imports discovered in A.15 review — must fix before Phase D. |
+| Builder has AI dependencies | A.15 documents builder AI imports; must fix before Phase D deletes `aipanel/`. Move `formatFileSize` to shared utility. |
+| Config migration for existing users | `waveai.json` and `aipresets.json` are simply ignored if not loaded. Existing files on disk are harmless. |
+| Upstream merge conflicts | Keeping `pkg/aiusechat/` intact (Phase D deferred) minimizes conflicts. Only wiring code is removed. |
+| Secret store still needed | `ssh:passwordsecretname` justifies keeping it. Documented in [[decisions.md#2026-05-12-secret-store--keep]]. |
diff --git a/.pi/todos.md b/.pi/todos.md
new file mode 100644
index 0000000000..477d1fc5f6
--- /dev/null
+++ b/.pi/todos.md
@@ -0,0 +1,110 @@
+# Active Tasks
+
+## Phase 1: Dev Environment ✅
+
+- [x] Install Task (build runner)
+- [x] Install Go 1.25+
+- [x] Run `task init` to install dependencies
+- [x] Run `task dev` — confirm app launches
+- [x] Run `task start` — confirm standalone build works
+- [x] Set up macOS CI workflow
+
+## Phase 2: Feature Planning
+
+- [ ] Finalize list of features to ADD
+- [x] Finalize list of features to REMOVE or DIMINISH
+- [ ] Prioritize implementation order
+
+### Features to Remove / Disable
+
+> "Remove" means **disable and hide from the UI** — don't delete code initially. Makes it easy to re-enable if needed and keeps the fork closer to upstream.
+
+- **All Wave AI features** — AI widgets, AI chat, AI presets, context-aware assistant, AI-related UI elements and settings
+
+## Phase 3: Implementation
+
+### High Priority — Bugfix
+
+- [x] **Crash on tab close after SSH session exit** — Fixed 2026-05-14
+ - Root cause found: double `DestroyBlockController` race in `CloseTab` (explicit goroutine + `DeleteTab` → `BlockCloseEvent` handler)
+ - Fix: removed redundant goroutine in `CloseTab`; added `sync.Once` to `ShellProc.Close()` as defense-in-depth
+ - Added trace logging to `CloseTab`, `DestroyBlockController`, `ShellController.Stop`, `DurableShellController.Stop`, `handleBlockCloseEvent`
+ - Tests: fixed 2 panicking tests (channel double-close bug in test code), all 14 tests pass under `-race`
+ - Spec: [[.pi/specs/bug-tabclose-crash.md]]
+ - [x] **Post-confirm cleanup:** Removed trace logging 2026-05-14
+
+### Features
+
+- [x] Remove telemetry (spec: [[.pi/specs/remove-telemetry.md]])
+ - [x] Phase A: Remove call sites
+ - [x] Phase B: Remove frontend telemetry
+ - [x] Phase C: Delete unused packages
+ - [x] Phase D: Clean up docs
+- [x] Remove Wave AI features (spec: [[.pi/specs/remove-waveai.md]])
+ - [x] Phase A: Disable UI (frontend) — completed 2026-05-16
+ - [x] Fix blank screen: invalid nested `` in `workspace.tsx` (removed inner PanelGroup but left VTabBar `` orphaned inside outer ``)
+ - [x] Remove sparkle/Claude icon from terminal block header (`getShellIntegrationIconButton` → no-op stub)
+ - [x] Minor: update misleading AI text in `builder-previewtab.tsx` EmptyStateView — fixed 2026-05-16
+ - [x] Phase B: Remove backend wiring (Go) — 2026-05-15
+ - [x] Phase C: Clean up docs & schemas — 2026-05-16
+ - [x] Phase D: Delete unused code — completed 2026-05-16
+ - [x] Remove builder AI dependencies (A.15: `AIPanel`, `WaveAIModel`, `formatFileSize`, `builder-focusmanager.ts`)
+ - [x] Move `formatFileSize` to shared utility (`@/util/util`) — completed in commit bd355fad
+ - [x] Delete `pkg/aiusechat/` (entire directory, ~12K lines, dead package)
+ - [x] Delete `frontend/app/aipanel/` (17 files, orphaned after builder deps removed)
+ - [x] Delete `frontend/app/view/waveai/`, `frontend/app/view/aifilediff/`, `frontend/app/view/waveconfig/waveaivisual.tsx`
+ - [x] Delete `frontend/app/onboarding/fakechat.tsx`, preview files
+ - [x] Clean Go structs: `SettingsType`, `MetaTSType`, `ObjRTInfo`, `FullConfigType`, `AIModeConfigType`, etc.
+ - [x] Delete default configs: `waveai.json`, `presets/ai.json`, clean `settings.json`
+ - [x] Regenerate auto-generated TS types (`gotypes.d.ts`, `waveevent.d.ts`, `wshclientapi.ts`) and Go metaconsts
+ - [x] Document Claude Code shell integration analysis for future pi agent reuse (`.pi/decisions.md`)
+- [ ] MOSH (Mobile Shell) support
+ - [ ] Research mosh client/server architecture and integration points
+ - [ ] Design ConnKeywords for MOSH connections (host, port, mosh-server path, etc.)
+ - [ ] Implement MOSH protocol handler (UDP session, mosh-client process management)
+ - [ ] Integrate with connection controller lifecycle
+ - [ ] Add UI support for MOSH connection type
+ - [ ] Handle coexistence with SSH port forwarding (MOSH doesn't support tunnels)
+ - [ ] Add tests
+ - [ ] Update documentation
+- [ ] SSH port forwarding (`LocalForward` / `RemoteForward`) (spec: [[.pi/specs/portforwarding.md]])
+ - [ ] Modify `pkg/wconfig/settingsconfig.go`
+ - [ ] Modify `pkg/remote/sshclient.go` (parse + return merged keywords)
+ - [ ] Modify `pkg/remote/conncontroller/conncontroller.go` (runtime forwarding)
+ - [ ] Update call sites for new `ConnectToClient` signature
+ - [ ] Add tests
+ - [ ] Update documentation (`docs/docs/connections.mdx`)
+- [ ] Paste screenshots into terminal
+ - [ ] Drag and drop images → SCP to remote, type fully-qualified filename into terminal
+ - [ ] Cmd+V paste clipboard image → upload as PNG, insert filename into terminal
+ - [ ] Consider implementing paste-as-image in Pi directly for tighter integration (avoid SCP+filename pattern, inject binary data or use OSC52/terminal-native paste)
+
+## Backlog / Ideas
+
+### Features to Add (discuss, spec, scope later)
+
+- **MOSH support** — Mobile shell for robust mobile/wifi connections. Note: MOSH doesn't support port forwarding, so SSH tunnels must coexist cleverly
+- **Vertical tabs** — Tab layout optimized for remote host switching
+
+
+
+### Forwarding Enhancements
+
+- DynamicForward (SOCKS proxy) — out of scope for v1, needs SOCKS5 handler
+- `wsh ssh -L` / `-R` CLI flags
+- UI status indicator for active port forwards
+
+### UX Improvements
+
+- **New block default connection** — Currently clicking '+' defaults to local; for remote-first workflow, should default to SSH/remote or at least not require manual switching
+- **SSH config as source of truth** — Connection management currently pushes users to JSON/settings UI instead of naturally leveraging `~/.ssh/config` as the primary management interface
+
+### File Transfer
+
+- **Paste screenshots into terminal** — Uploads the image to the remote server and pastes the filename/path into the terminal window
+- **Drag and drop file transfer** — Drag files into the file browser to upload; drag from file browser to download
+
+### General
+
+- Remove checks to `dl.waveterm.dev` (e.g., update checks, download URLs)
+- Evaluate which other local-first widgets to remove/diminish
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000000..64bf1784d6
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,81 @@
+# AGENTS.md — waveterm-remote Fork
+
+This fork of Wave Terminal is optimized for remote development workflows. The local machine is a thin client; remote SSH environments are primary workspaces.
+
+## Git Remotes
+
+- `origin` → `https://github.com/whoisjeremylam/waveterm-remote` (this fork)
+- `upstream` → `https://github.com/wavetermdev/waveterm` (original)
+- Do not run `git push` — the user handles pushes interactively with 2FA
+
+## Dev Environment
+
+| Tool | Status |
+|------|--------|
+| NodeJS v24.14.0 | Available |
+| npm 11.9.0 | Available |
+| git 2.43.0 | Available |
+| Go 1.26.2 | Local install in `golang-1.26.2/` |
+| Task (build runner) | Local npm dep (`@go-task/cli`) |
+
+Go and Task are installed locally (not globally). The Taskfile uses `{{.GO_DIR}}` and `{{.GO}}` vars to reference the local Go binary.
+
+**When upgrading Go**: download to `golang-/`, update `GO_DIR` in Taskfile.yml vars, and run `echo "module golang" > golang-/go.mod` (prevents `go mod tidy` from scanning the Go install dir as part of the project module).
+
+Run `./node_modules/.bin/task init` then `./node_modules/.bin/task dev`.
+
+## Planning Documents
+
+All fork planning lives in `.pi/`:
+- `.pi/index.md` — entry point
+- `.pi/context.md` — fork purpose and problem statement
+- `.pi/todos.md` — active tasks and backlog
+- `.pi/decisions.md` — architecture decisions
+- `.pi/specs/` — feature specifications
+
+Current active spec: `.pi/specs/portforwarding.md`
+
+## Architecture
+
+- **Frontend**: React/TypeScript in `frontend/`
+- **Backend**: Go in `pkg/` and `cmd/`
+- **Electron main**: `emain/` (Node.js bridge between frontend and Go)
+- **Go backend runs as separate process** — Electron main process bridges to it via IPC
+
+## Priorities
+
+1. Verify `task dev` and `task start` work (build tools installed)
+2. Implement SSH port forwarding (`LocalForward`/`RemoteForward`) — spec ready
+3. Later: remove/disable AI features, MOSH support, vertical tabs, UX improvements
+
+## Conventions
+
+- Follow existing code patterns: `panichandler` on goroutines, `WithLock` for struct mutations, table-driven tests with `t.Run`, manual `if` assertions (no testify)
+- `docs/docs/` is public-facing documentation (Docusaurus) — do not mix fork planning with user docs
+- `README.md` stays close to upstream; fork differences go in `.pi/` or `README-FORK.md` if needed
+- All new SSH config keywords follow the parsing pattern in `pkg/remote/sshclient.go`
+- ConnKeywords fields use `json:"ssh:..."` tags for SSH config and `json:"conn:..."` for internal config
+
+## Key Files for SSH Work
+
+| File | Purpose |
+|------|---------|
+| `pkg/wconfig/settingsconfig.go` | `ConnKeywords` struct — add new SSH fields here |
+| `pkg/remote/sshclient.go` | Config parsing (`findSshConfigKeywords`), merging (`mergeKeywords`), `ConnectToClient` |
+| `pkg/remote/conncontroller/conncontroller.go` | Connection lifecycle — start forwarding after connect, cleanup on disconnect |
+| `pkg/genconn/ssh-impl.go` | SSH session implementation |
+| `cmd/wsh/cmd/wshcmd-ssh.go` | `wsh ssh` CLI command |
+| `docs/docs/connections.mdx` | Public docs for connections and SSH config |
+
+## Testing
+
+- No existing tests for `sshclient.go` or `conncontroller.go` — new tests would be first coverage
+- Use `t.TempDir()` for filesystem fixtures, not external fixture files
+- Use hand-written inline mocks, not gomock
+- `t.Parallel()` on independent tests only
+
+## Out of Scope (Current)
+
+- DynamicForward (needs SOCKS5 handler)
+- `wsh ssh -L`/`-R` CLI flags
+- UI status indicators for port forwarding
diff --git a/README.md b/README.md
index a9f406725c..1b547514b4 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,8 @@
+> **Fork:** This is a fork of [Wave Terminal](https://github.com/wavetermdev/waveterm) optimized for remote development workflows.
+
# Wave Terminal
@@ -17,23 +19,28 @@
-[](https://app.fossa.com/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm?ref=badge_shield)
+Wave is an open-source terminal for macOS, Linux, and Windows. No accounts required.
+
+Wave supports durable SSH sessions that survive network interruptions and restarts, with automatic reconnection. Edit remote files with a built-in graphical editor and preview files inline without leaving the terminal.
+
+## Fork Notes
-Wave is an open-source, AI-integrated terminal for macOS, Linux, and Windows. It works with any AI model. Bring your own API keys for OpenAI, Claude, or Gemini, or run local models via Ollama and LM Studio. No accounts required.
+This fork is optimized for remote development workflows with a focus on macOS.
-Wave also supports durable SSH sessions that survive network interruptions and restarts, with automatic reconnection. Edit remote files with a built-in graphical editor and preview files inline without leaving the terminal.
+- **No telemetry** — All analytics, telemetry, and cloud data collection have been completely removed; no usage data is sent to external servers
+- **Local toolchain** — Go and Task are installed locally (not global), no system dependencies required
+- **macOS builds** — CI builds macOS `.dmg` via GitHub Actions (manual trigger)
+- **Planned changes** — SSH port forwarding, remove unnecessary AI features, MOSH support, vertical tabs, SSH config as source of truth for connections

## Key Features
-- Wave AI - Context-aware terminal assistant that reads your terminal output, analyzes widgets, and performs file operations
- Durable SSH Sessions - Remote terminal sessions survive connection interruptions, network changes, and Wave restarts with automatic reconnection
-- Flexible drag & drop interface to organize terminal blocks, editors, web browsers, and AI assistants
+- Flexible drag & drop interface to organize terminal blocks, editors, web browsers, and previews
- Built-in editor for editing remote files with syntax highlighting and modern editor features
- Rich file preview system for remote files (markdown, images, video, PDFs, CSVs, directories)
- Quick full-screen toggle for any block - expand terminals, editors, and previews for better visibility, then instantly return to multi-block view
-- AI chat widget with support for multiple models (OpenAI, Claude, Azure, Perplexity, Ollama)
- Command Blocks for isolating and monitoring individual commands
- One-click remote connections with full terminal and file system access
- Secure secret storage using native system backends - store API keys and credentials locally, access them across SSH sessions
@@ -41,28 +48,10 @@ Wave also supports durable SSH sessions that survive network interruptions and r
- Powerful `wsh` command system for managing your workspace from the CLI and sharing data between terminal sessions
- Connected file management with `wsh file` - seamlessly copy and sync files between local and remote SSH hosts
-## Wave AI
-
-Wave AI is your context-aware terminal assistant with access to your workspace:
-
-- **Terminal Context**: Reads terminal output and scrollback for debugging and analysis
-- **File Operations**: Read, write, and edit files with automatic backups and user approval
-- **CLI Integration**: Use `wsh ai` to pipe output or attach files directly from the command line
-- **BYOK Support**: Bring your own API keys for OpenAI, Claude, Gemini, Azure, and other providers
-- **Local Models**: Run local models with Ollama, LM Studio, and other OpenAI-compatible providers
-- **Free Beta**: Included AI credits while we refine the experience
-- **Coming Soon**: Command execution (with approval)
-
-Learn more in our [Wave AI documentation](https://docs.waveterm.dev/waveai) and [Wave AI Modes documentation](https://docs.waveterm.dev/waveai-modes).
-
## Installation
Wave Terminal works on macOS, Linux, and Windows.
-Platform-specific installation instructions can be found [here](https://docs.waveterm.dev/gettingstarted).
-
-You can also install Wave Terminal directly from: [www.waveterm.dev/download](https://www.waveterm.dev/download).
-
### Minimum requirements
Wave Terminal runs on the following platforms:
@@ -77,20 +66,6 @@ The WSH helper runs on the following platforms:
- Windows 10 or later (x64)
- Linux Kernel 2.6.32 or later (x64), Linux Kernel 3.1 or later (arm64)
-## Roadmap
-
-Wave is constantly improving! Our roadmap will be continuously updated with our goals for each release. You can find it [here](./ROADMAP.md).
-
-Want to provide input to our future releases? Connect with us on [Discord](https://discord.gg/XfvZ334gwU) or open a [Feature Request](https://github.com/wavetermdev/waveterm/issues/new/choose)!
-
-## Links
-
-- Homepage — https://www.waveterm.dev
-- Download Page — https://www.waveterm.dev/download
-- Documentation — https://docs.waveterm.dev
-- X — https://x.com/wavetermdev
-- Discord Community — https://discord.gg/XfvZ334gwU
-
## Building from Source
See [Building Wave Terminal](BUILD.md).
@@ -104,14 +79,6 @@ Find more information in our [Contributions Guide](CONTRIBUTING.md), which inclu
- [Ways to contribute](CONTRIBUTING.md#contributing-to-wave-terminal)
- [Contribution guidelines](CONTRIBUTING.md#before-you-start)
-### Sponsoring Wave ❤️
-
-If Wave Terminal is useful to you or your company, consider sponsoring development.
-
-Sponsorship helps support the time spent building and maintaining the project.
-
-- https://github.com/sponsors/wavetermdev
-
## License
Wave Terminal is licensed under the Apache-2.0 License. For more information on our dependencies, see [here](./ACKNOWLEDGEMENTS.md).
diff --git a/Taskfile.yml b/Taskfile.yml
index bf37a83e45..76154d1b91 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -13,6 +13,10 @@ vars:
ARTIFACTS_BUCKET: waveterm-github-artifacts/staging-w2
RELEASES_BUCKET: dl.waveterm.dev/releases-w2
WINGET_PACKAGE: CommandLine.Wave
+ GO_DIR: "golang-1.26.2"
+ GO: "{{.ROOT_DIR}}/{{.GO_DIR}}/bin/go"
+ ZIG_DIR: "zig-0.14.0"
+ ZIG: "{{.ROOT_DIR}}/{{.ZIG_DIR}}/zig"
tasks:
electron:dev:
@@ -166,7 +170,7 @@ tasks:
generates:
- "dist/schema/**/*"
cmds:
- - go run cmd/generateschema/main-generateschema.go
+ - "{{.GO}} run cmd/generateschema/main-generateschema.go"
- cmd: '{{.RMRF}} "dist/schema"'
ignore_error: true
- task: copyfiles:'schema':'dist/schema'
@@ -225,7 +229,7 @@ tasks:
- task: build:server:internal
vars:
ARCHS: amd64
- GO_ENV_VARS: CC="zig cc -target x86_64-windows-gnu"
+ GO_ENV_VARS: CC="{{.ZIG}} cc -target x86_64-windows-gnu"
deps:
- go:mod:tidy
sources:
@@ -248,7 +252,7 @@ tasks:
ARCHS:
sh: echo {{if eq "arm" ARCH}}arm64{{else}}{{ARCH}}{{end}}
GO_ENV_VARS:
- sh: echo "{{if eq "amd64" ARCH}}CC=\"zig cc -target x86_64-windows-gnu\"{{else}}CC=\"zig cc -target aarch64-windows-gnu\"{{end}}"
+ sh: echo "{{if eq "amd64" ARCH}}CC=\"{{.ZIG}} cc -target x86_64-windows-gnu\"{{else}}CC=\"{{.ZIG}} cc -target aarch64-windows-gnu\"{{end}}"
build:server:linux:
desc: Build the wavesrv component for Linux platforms (only generates artifacts for the current architecture).
@@ -261,14 +265,14 @@ tasks:
ARCHS:
sh: echo {{if eq "arm" ARCH}}arm64{{else}}{{ARCH}}{{end}}
GO_ENV_VARS:
- sh: echo "{{if eq "amd64" ARCH}}CC=\"zig cc -target x86_64-linux-gnu.2.28\"{{else}}CC=\"zig cc -target aarch64-linux-gnu.2.28\"{{end}}"
+ sh: echo "{{if eq "amd64" ARCH}}CC=\"{{.ZIG}} cc -target x86_64-linux-gnu.2.28\"{{else}}CC=\"{{.ZIG}} cc -target aarch64-linux-gnu.2.28\"{{end}}"
build:server:internal:
requires:
vars:
- ARCHS
cmd:
- cmd: CGO_ENABLED=1 GOARCH={{.GOARCH}} {{.GO_ENV_VARS}} go build -tags "osusergo,sqlite_omit_load_extension" -ldflags "{{.GO_LDFLAGS}} -X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.WaveVersion={{.VERSION}}" -o dist/bin/wavesrv.{{if eq .GOARCH "amd64"}}x64{{else}}{{.GOARCH}}{{end}}{{exeExt}} cmd/server/main-server.go
+ cmd: CGO_ENABLED=1 GOARCH={{.GOARCH}} {{.GO_ENV_VARS}} {{.GO}} build -tags "osusergo,sqlite_omit_load_extension" -ldflags "{{.GO_LDFLAGS}} -X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.WaveVersion={{.VERSION}}" -o dist/bin/wavesrv.{{if eq .GOARCH "amd64"}}x64{{else}}{{.GOARCH}}{{end}}{{exeExt}} cmd/server/main-server.go
for:
var: ARCHS
split: ","
@@ -342,7 +346,7 @@ tasks:
- GOOS
- GOARCH
- VERSION
- cmd: (CGO_ENABLED=0 GOOS={{.GOOS}} GOARCH={{.GOARCH}} go build -ldflags="-s -w -X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.WaveVersion={{.VERSION}}" -o dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.NORMALIZEDARCH}}{{.EXT}} cmd/wsh/main-wsh.go)
+ cmd: (CGO_ENABLED=0 GOOS={{.GOOS}} GOARCH={{.GOARCH}} {{.GO}} build -ldflags="-s -w -X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.WaveVersion={{.VERSION}}" -o dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.NORMALIZEDARCH}}{{.EXT}} cmd/wsh/main-wsh.go)
internal: true
build:tsunamiscaffold:
@@ -363,8 +367,8 @@ tasks:
generate:
desc: Generate Typescript bindings for the Go backend.
cmds:
- - go run cmd/generatets/main-generatets.go
- - go run cmd/generatego/main-generatego.go
+ - "{{.GO}} run cmd/generatets/main-generatets.go"
+ - "{{.GO}} run cmd/generatego/main-generatego.go"
deps:
- build:schema
sources:
@@ -469,7 +473,7 @@ tasks:
desc: Initialize the project for development.
cmds:
- npm install
- - go mod tidy
+ - "{{.GO}} mod tidy"
- cd docs && npm install
dev:cleardata:windows:
@@ -505,7 +509,7 @@ tasks:
- go.sum
sources:
- go.mod
- cmd: go mod tidy
+ cmd: "{{.GO}} mod tidy"
copyfiles:*:*:
desc: Recursively copy directory and its contents.
@@ -522,7 +526,7 @@ tasks:
tsunami:demo:todo:
desc: Run the tsunami todo demo application
- cmd: go run demo/todo/*.go
+ cmd: "{{.GO}} run demo/todo/*.go"
dir: tsunami
env:
TSUNAMI_LISTENADDR: "localhost:12026"
@@ -632,7 +636,7 @@ tasks:
platforms: [windows]
ignore_error: true
- mkdir -p bin
- - cd tsunami && go build -ldflags "-X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.TsunamiVersion={{.VERSION}}" -o ../bin/tsunami{{exeExt}} cmd/main-tsunami.go
+ - cd tsunami && {{.GO}} build -ldflags "-X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.TsunamiVersion={{.VERSION}}" -o ../bin/tsunami{{exeExt}} cmd/main-tsunami.go
sources:
- "tsunami/**/*.go"
- "tsunami/go.mod"
@@ -651,9 +655,9 @@ tasks:
godoc:
desc: Start the Go documentation server for the root module
- cmd: $(go env GOPATH)/bin/pkgsite -http=:6060
+ cmd: $({{.GO}} env GOPATH)/bin/pkgsite -http=:6060
tsunami:godoc:
desc: Start the Go documentation server for the tsunami module
- cmd: $(go env GOPATH)/bin/pkgsite -http=:6060
+ cmd: $({{.GO}} env GOPATH)/bin/pkgsite -http=:6060
dir: tsunami
diff --git a/cmd/generatego/main-generatego.go b/cmd/generatego/main-generatego.go
index ab7e338439..99e268ad85 100644
--- a/cmd/generatego/main-generatego.go
+++ b/cmd/generatego/main-generatego.go
@@ -24,9 +24,7 @@ func GenerateWshClient() error {
fmt.Fprintf(os.Stderr, "generating wshclient file to %s\n", WshClientFileName)
var buf strings.Builder
gogen.GenerateBoilerplate(&buf, "wshclient", []string{
- "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes",
"github.com/wavetermdev/waveterm/pkg/baseds",
- "github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata",
"github.com/wavetermdev/waveterm/pkg/vdom",
"github.com/wavetermdev/waveterm/pkg/waveobj",
"github.com/wavetermdev/waveterm/pkg/wconfig",
diff --git a/cmd/generateschema/main-generateschema.go b/cmd/generateschema/main-generateschema.go
index dd24a4df0d..c9e77f4d79 100644
--- a/cmd/generateschema/main-generateschema.go
+++ b/cmd/generateschema/main-generateschema.go
@@ -18,10 +18,8 @@ import (
const WaveSchemaSettingsFileName = "schema/settings.json"
const WaveSchemaConnectionsFileName = "schema/connections.json"
-const WaveSchemaAiPresetsFileName = "schema/aipresets.json"
const WaveSchemaWidgetsFileName = "schema/widgets.json"
const WaveSchemaBackgroundsFileName = "schema/backgrounds.json"
-const WaveSchemaWaveAIFileName = "schema/waveai.json"
// ViewNameType is a string type whose JSON Schema offers enum suggestions for the most
// common widget view names while still accepting any arbitrary string value.
@@ -193,12 +191,6 @@ func main() {
log.Fatalf("connections schema error: %v", err)
}
- aiPresetsTemplate := make(map[string]wconfig.AiSettingsType)
- err = generateSchema(&aiPresetsTemplate, WaveSchemaAiPresetsFileName, false)
- if err != nil {
- log.Fatalf("ai presets schema error: %v", err)
- }
-
err = generateWidgetsSchema(WaveSchemaWidgetsFileName)
if err != nil {
log.Fatalf("widgets schema error: %v", err)
@@ -210,9 +202,5 @@ func main() {
log.Fatalf("backgrounds schema error: %v", err)
}
- waveAITemplate := make(map[string]wconfig.AIModeConfigType)
- err = generateSchema(&waveAITemplate, WaveSchemaWaveAIFileName, false)
- if err != nil {
- log.Fatalf("waveai schema error: %v", err)
- }
+
}
diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go
index b204643ee8..5e8593c424 100644
--- a/cmd/server/main-server.go
+++ b/cmd/server/main-server.go
@@ -14,7 +14,6 @@ import (
"time"
"github.com/joho/godotenv"
- "github.com/wavetermdev/waveterm/pkg/aiusechat"
"github.com/wavetermdev/waveterm/pkg/authkey"
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
@@ -22,19 +21,13 @@ import (
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/jobcontroller"
"github.com/wavetermdev/waveterm/pkg/panichandler"
- "github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/wshfs"
- "github.com/wavetermdev/waveterm/pkg/secretstore"
"github.com/wavetermdev/waveterm/pkg/service"
- "github.com/wavetermdev/waveterm/pkg/telemetry"
- "github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
"github.com/wavetermdev/waveterm/pkg/util/envutil"
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/util/sigutil"
- "github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj"
- "github.com/wavetermdev/waveterm/pkg/wcloud"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wcore"
"github.com/wavetermdev/waveterm/pkg/web"
@@ -44,7 +37,6 @@ import (
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshremote"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshserver"
"github.com/wavetermdev/waveterm/pkg/wshutil"
- "github.com/wavetermdev/waveterm/pkg/wslconn"
"github.com/wavetermdev/waveterm/pkg/wstore"
"net/http"
@@ -55,15 +47,8 @@ import (
var WaveVersion = "0.0.0"
var BuildTime = "0"
-const InitialTelemetryWait = 10 * time.Second
-const TelemetryTick = 2 * time.Minute
-const TelemetryInterval = 4 * time.Hour
-const TelemetryInitialCountsWait = 5 * time.Second
-const TelemetryCountsInterval = 1 * time.Hour
const BackupCleanupTick = 2 * time.Minute
const BackupCleanupInterval = 4 * time.Hour
-const InitialDiagnosticWait = 5 * time.Minute
-const DiagnosticTick = 10 * time.Minute
var shutdownOnce sync.Once
@@ -81,8 +66,6 @@ func doShutdown(reason string) {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
go blockcontroller.StopAllBlockControllersForShutdown()
- shutdownActivityUpdate()
- sendTelemetryWrapper()
// TODO deal with flush in progress
clearTempFiles()
filestore.WFS.FlushCache(ctx)
@@ -118,74 +101,6 @@ func startConfigWatcher() {
}
}
-func telemetryLoop() {
- defer func() {
- panichandler.PanicHandler("telemetryLoop", recover())
- }()
- var nextSend int64
- time.Sleep(InitialTelemetryWait)
- for {
- if time.Now().Unix() > nextSend {
- nextSend = time.Now().Add(TelemetryInterval).Unix()
- sendTelemetryWrapper()
- }
- time.Sleep(TelemetryTick)
- }
-}
-
-func diagnosticLoop() {
- defer func() {
- panichandler.PanicHandler("diagnosticLoop", recover())
- }()
- if os.Getenv("WAVETERM_NOPING") != "" {
- log.Printf("WAVETERM_NOPING set, disabling diagnostic ping\n")
- return
- }
- var lastSentDate string
- time.Sleep(InitialDiagnosticWait)
- for {
- currentDate := time.Now().Format("2006-01-02")
- if lastSentDate == "" || lastSentDate != currentDate {
- if sendDiagnosticPing() {
- lastSentDate = currentDate
- }
- }
- time.Sleep(DiagnosticTick)
- }
-}
-
-func sendDiagnosticPing() bool {
- ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancelFn()
-
- rpcClient := wshclient.GetBareRpcClient()
- isOnline, err := wshclient.NetworkOnlineCommand(rpcClient, &wshrpc.RpcOpts{Route: "electron", Timeout: 2000})
- if err != nil || !isOnline {
- return false
- }
- clientId := wstore.GetClientId()
- usageTelemetry := telemetry.IsTelemetryEnabled()
- wcloud.SendDiagnosticPing(ctx, clientId, usageTelemetry)
- return true
-}
-
-func setupTelemetryConfigHandler() {
- watcher := wconfig.GetWatcher()
- if watcher == nil {
- return
- }
- currentConfig := watcher.GetFullConfig()
- currentTelemetryEnabled := currentConfig.Settings.TelemetryEnabled
-
- watcher.RegisterUpdateHandler(func(newConfig wconfig.FullConfigType) {
- newTelemetryEnabled := newConfig.Settings.TelemetryEnabled
- if newTelemetryEnabled != currentTelemetryEnabled {
- currentTelemetryEnabled = newTelemetryEnabled
- wcore.GoSendNoTelemetryUpdate(newTelemetryEnabled)
- }
- })
-}
-
func backupCleanupLoop() {
defer func() {
panichandler.PanicHandler("backupCleanupLoop", recover())
@@ -203,185 +118,6 @@ func backupCleanupLoop() {
}
}
-func panicTelemetryHandler(panicName string) {
- activity := wshrpc.ActivityUpdate{NumPanics: 1}
- err := telemetry.UpdateActivity(context.Background(), activity)
- if err != nil {
- log.Printf("error updating activity (panicTelemetryHandler): %v\n", err)
- }
- telemetry.RecordTEvent(context.Background(), telemetrydata.MakeTEvent("debug:panic", telemetrydata.TEventProps{
- PanicType: panicName,
- }))
-}
-
-func sendTelemetryWrapper() {
- defer func() {
- panichandler.PanicHandler("sendTelemetryWrapper", recover())
- }()
- ctx, cancelFn := context.WithTimeout(context.Background(), 15*time.Second)
- defer cancelFn()
- beforeSendActivityUpdate(ctx)
- clientId := wstore.GetClientId()
- err := wcloud.SendAllTelemetry(clientId)
- if err != nil {
- log.Printf("[error] sending telemetry: %v\n", err)
- }
-}
-
-func updateTelemetryCounts(lastCounts telemetrydata.TEventProps) telemetrydata.TEventProps {
- ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancelFn()
- var props telemetrydata.TEventProps
- props.CountBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx)
- props.CountTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
- props.CountWindows, _ = wstore.DBGetCount[*waveobj.Window](ctx)
- props.CountWorkspaces, _, _ = wstore.DBGetWSCounts(ctx)
- props.CountSSHConn = conncontroller.GetNumSSHHasConnected()
- props.CountWSLConn = wslconn.GetNumWSLHasConnected()
- props.CountJobs = jobcontroller.GetNumJobsRunning()
- props.CountJobsConnected = jobcontroller.GetNumJobsConnected()
- props.CountViews, _ = wstore.DBGetBlockViewCounts(ctx)
-
- fullConfig := wconfig.GetWatcher().GetFullConfig()
- customWidgets := fullConfig.CountCustomWidgets()
- customAIPresets := fullConfig.CountCustomAIPresets()
- customSettings := wconfig.CountCustomSettings()
- customAIModes := fullConfig.CountCustomAIModes()
-
- props.UserSet = &telemetrydata.TEventUserProps{
- SettingsCustomWidgets: customWidgets,
- SettingsCustomAIPresets: customAIPresets,
- SettingsCustomSettings: customSettings,
- SettingsCustomAIModes: customAIModes,
- }
-
- secretsCount, err := secretstore.CountSecrets()
- if err == nil {
- props.UserSet.SettingsSecretsCount = secretsCount
- }
-
- if utilfn.CompareAsMarshaledJson(props, lastCounts) {
- return lastCounts
- }
- tevent := telemetrydata.MakeTEvent("app:counts", props)
- err = telemetry.RecordTEvent(ctx, tevent)
- if err != nil {
- log.Printf("error recording counts tevent: %v\n", err)
- }
- return props
-}
-
-func updateTelemetryCountsLoop() {
- defer func() {
- panichandler.PanicHandler("updateTelemetryCountsLoop", recover())
- }()
- var nextSend int64
- var lastCounts telemetrydata.TEventProps
- time.Sleep(TelemetryInitialCountsWait)
- for {
- if time.Now().Unix() > nextSend {
- nextSend = time.Now().Add(TelemetryCountsInterval).Unix()
- lastCounts = updateTelemetryCounts(lastCounts)
- }
- time.Sleep(TelemetryTick)
- }
-}
-
-func beforeSendActivityUpdate(ctx context.Context) {
- activity := wshrpc.ActivityUpdate{}
- activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
- activity.NumBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx)
- activity.Blocks, _ = wstore.DBGetBlockViewCounts(ctx)
- activity.NumWindows, _ = wstore.DBGetCount[*waveobj.Window](ctx)
- activity.NumSSHConn = conncontroller.GetNumSSHHasConnected()
- activity.NumWSLConn = wslconn.GetNumWSLHasConnected()
- activity.NumWSNamed, activity.NumWS, _ = wstore.DBGetWSCounts(ctx)
- err := telemetry.UpdateActivity(ctx, activity)
- if err != nil {
- log.Printf("error updating before activity: %v\n", err)
- }
-}
-
-func startupActivityUpdate(firstLaunch bool) {
- defer func() {
- panichandler.PanicHandler("startupActivityUpdate", recover())
- }()
- ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancelFn()
- activity := wshrpc.ActivityUpdate{Startup: 1}
- err := telemetry.UpdateActivity(ctx, activity) // set at least one record into activity (don't use go routine wrap here)
- if err != nil {
- log.Printf("error updating startup activity: %v\n", err)
- }
- autoUpdateChannel := telemetry.AutoUpdateChannel()
- autoUpdateEnabled := telemetry.IsAutoUpdateEnabled()
- shellType, shellVersion, shellErr := shellutil.DetectShellTypeAndVersion()
- if shellErr != nil {
- shellType = "error"
- shellVersion = ""
- }
- userSetOnce := &telemetrydata.TEventUserProps{
- ClientInitialVersion: "v" + WaveVersion,
- }
- tosTs := telemetry.GetTosAgreedTs()
- var cohortTime time.Time
- if tosTs > 0 {
- cohortTime = time.UnixMilli(tosTs)
- } else {
- cohortTime = time.Now()
- }
- cohortMonth := cohortTime.Format("2006-01")
- year, week := cohortTime.ISOWeek()
- cohortISOWeek := fmt.Sprintf("%04d-W%02d", year, week)
- userSetOnce.CohortMonth = cohortMonth
- userSetOnce.CohortISOWeek = cohortISOWeek
- fullConfig := wconfig.GetWatcher().GetFullConfig()
- props := telemetrydata.TEventProps{
- UserSet: &telemetrydata.TEventUserProps{
- ClientVersion: "v" + wavebase.WaveVersion,
- ClientBuildTime: wavebase.BuildTime,
- ClientArch: wavebase.ClientArch(),
- ClientOSRelease: wavebase.UnameKernelRelease(),
- ClientIsDev: wavebase.IsDevMode(),
- ClientPackageType: wavebase.ClientPackageType(),
- ClientMacOSVersion: wavebase.ClientMacOSVersion(),
- AutoUpdateChannel: autoUpdateChannel,
- AutoUpdateEnabled: autoUpdateEnabled,
- LocalShellType: shellType,
- LocalShellVersion: shellVersion,
- SettingsTransparent: fullConfig.Settings.WindowTransparent,
- },
- UserSetOnce: userSetOnce,
- }
- if firstLaunch {
- props.AppFirstLaunch = true
- }
- tevent := telemetrydata.MakeTEvent("app:startup", props)
- err = telemetry.RecordTEvent(ctx, tevent)
- if err != nil {
- log.Printf("error recording startup event: %v\n", err)
- }
-}
-
-func shutdownActivityUpdate() {
- ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second)
- defer cancelFn()
- activity := wshrpc.ActivityUpdate{Shutdown: 1}
- err := telemetry.UpdateActivity(ctx, activity) // do NOT use the go routine wrap here (this needs to be synchronous)
- if err != nil {
- log.Printf("error updating shutdown activity: %v\n", err)
- }
- err = telemetry.TruncateActivityTEventForShutdown(ctx)
- if err != nil {
- log.Printf("error truncating activity t-event for shutdown: %v\n", err)
- }
- tevent := telemetrydata.MakeTEvent("app:shutdown", telemetrydata.TEventProps{})
- err = telemetry.RecordTEvent(ctx, tevent)
- if err != nil {
- log.Printf("error recording shutdown event: %v\n", err)
- }
-}
-
func createMainWshClient() {
rpc := wshserver.GetMainRpcClient()
wshutil.DefaultRouter.RegisterTrustedLeaf(rpc, wshutil.DefaultRoute)
@@ -405,11 +141,6 @@ func grabAndRemoveEnvVars() error {
if err != nil {
return err
}
- err = wcloud.CacheAndRemoveEnvVars()
- if err != nil {
- return err
- }
-
// Remove WAVETERM env vars that leak from prod => dev
os.Unsetenv("WAVETERM_CLIENTID")
os.Unsetenv("WAVETERM_WORKSPACEID")
@@ -525,7 +256,6 @@ func main() {
log.Printf("error initializing wstore: %v\n", err)
return
}
- panichandler.PanicTelemetryHandler = panicTelemetryHandler
go func() {
defer func() {
panichandler.PanicHandler("InitCustomShellStartupFiles", recover())
@@ -563,15 +293,9 @@ func main() {
sigutil.InstallSIGUSR1Handler()
wconfig.MigratePresetsBackgrounds()
startConfigWatcher()
- aiusechat.InitAIModeConfigWatcher()
maybeStartPprofServer()
go stdinReadWatch()
- go telemetryLoop()
- go diagnosticLoop()
- setupTelemetryConfigHandler()
- go updateTelemetryCountsLoop()
go backupCleanupLoop()
- go startupActivityUpdate(firstLaunch) // must be after startConfigWatcher()
blocklogger.InitBlockLogger()
jobcontroller.InitJobController()
blockcontroller.InitBlockController()
diff --git a/cmd/testai/main-testai.go b/cmd/testai/main-testai.go
deleted file mode 100644
index 606e6ac6a1..0000000000
--- a/cmd/testai/main-testai.go
+++ /dev/null
@@ -1,548 +0,0 @@
-// Copyright 2025, Command Line Inc.
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import (
- "context"
- _ "embed"
- "encoding/json"
- "flag"
- "fmt"
- "log"
- "net/http"
- "os"
- "time"
-
- "github.com/google/uuid"
- "github.com/wavetermdev/waveterm/pkg/aiusechat"
- "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
- "github.com/wavetermdev/waveterm/pkg/web/sse"
-)
-
-//go:embed testschema.json
-var testSchemaJSON string
-
-const (
- DefaultAnthropicModel = "claude-sonnet-4-5"
- DefaultOpenAIModel = "gpt-5.1"
- DefaultOpenRouterModel = "mistralai/mistral-small-3.2-24b-instruct"
- DefaultNanoGPTModel = "zai-org/glm-4.7"
- DefaultGeminiModel = "gemini-3-pro-preview"
-)
-
-// TestResponseWriter implements http.ResponseWriter and additional interfaces for testing
-type TestResponseWriter struct {
- header http.Header
-}
-
-func (w *TestResponseWriter) Header() http.Header {
- if w.header == nil {
- w.header = make(http.Header)
- }
- return w.header
-}
-
-func (w *TestResponseWriter) Write(data []byte) (int, error) {
- fmt.Printf("SSE: %s", string(data))
- return len(data), nil
-}
-
-func (w *TestResponseWriter) WriteHeader(statusCode int) {
- fmt.Printf("Status: %d\n", statusCode)
-}
-
-// Implement http.Flusher interface
-func (w *TestResponseWriter) Flush() {
- // No-op for testing
-}
-
-// Implement interfaces needed by http.ResponseController
-func (w *TestResponseWriter) SetWriteDeadline(deadline time.Time) error {
- // No-op for testing
- return nil
-}
-
-func (w *TestResponseWriter) SetReadDeadline(deadline time.Time) error {
- // No-op for testing
- return nil
-}
-
-func getToolDefinitions() []uctypes.ToolDefinition {
- var schemas map[string]any
- if err := json.Unmarshal([]byte(testSchemaJSON), &schemas); err != nil {
- log.Printf("Error parsing schema: %v\n", err)
- return nil
- }
-
- var configSchema map[string]any
- if rawSchema, ok := schemas["config"]; ok && rawSchema != nil {
- if schema, ok := rawSchema.(map[string]any); ok {
- configSchema = schema
- }
- }
- if configSchema == nil {
- configSchema = map[string]any{"type": "object"}
- }
-
- return []uctypes.ToolDefinition{
- {
- Name: "get_config",
- Description: "Get the current GitHub Actions Monitor configuration settings including repository, workflow, polling interval, and max workflow runs",
- InputSchema: map[string]any{
- "type": "object",
- },
- },
- {
- Name: "update_config",
- Description: "Update GitHub Actions Monitor configuration settings",
- InputSchema: configSchema,
- },
- {
- Name: "get_data",
- Description: "Get the current GitHub Actions workflow run data including workflow runs, loading state, and errors",
- InputSchema: map[string]any{
- "type": "object",
- },
- },
- }
-}
-
-func testOpenAI(ctx context.Context, model, message string, tools []uctypes.ToolDefinition) {
- apiKey := os.Getenv("OPENAI_APIKEY")
- if apiKey == "" {
- fmt.Println("Error: OPENAI_APIKEY environment variable not set")
- os.Exit(1)
- }
-
- opts := &uctypes.AIOptsType{
- APIType: uctypes.APIType_OpenAIResponses,
- APIToken: apiKey,
- Model: model,
- MaxTokens: 4096,
- ThinkingLevel: uctypes.ThinkingLevelMedium,
- }
-
- // Generate a chat ID
- chatID := uuid.New().String()
-
- // Convert to AIMessage format for WaveAIPostMessageWrap
- aiMessage := &uctypes.AIMessage{
- MessageId: uuid.New().String(),
- Parts: []uctypes.AIMessagePart{
- {
- Type: uctypes.AIMessagePartTypeText,
- Text: message,
- },
- },
- }
-
- fmt.Printf("Testing OpenAI streaming with WaveAIPostMessageWrap, model: %s\n", model)
- fmt.Printf("Message: %s\n", message)
- fmt.Printf("Chat ID: %s\n", chatID)
- fmt.Println("---")
-
- testWriter := &TestResponseWriter{}
- sseHandler := sse.MakeSSEHandlerCh(testWriter, ctx)
- defer sseHandler.Close()
-
- chatOpts := uctypes.WaveChatOpts{
- ChatId: chatID,
- ClientId: uuid.New().String(),
- Config: *opts,
- Tools: tools,
- }
- err := aiusechat.WaveAIPostMessageWrap(ctx, sseHandler, aiMessage, chatOpts)
- if err != nil {
- fmt.Printf("OpenAI streaming error: %v\n", err)
- }
-}
-
-func testOpenAIComp(ctx context.Context, model, message string, tools []uctypes.ToolDefinition) {
- apiKey := os.Getenv("OPENAI_APIKEY")
- if apiKey == "" {
- fmt.Println("Error: OPENAI_APIKEY environment variable not set")
- os.Exit(1)
- }
-
- opts := &uctypes.AIOptsType{
- APIType: uctypes.APIType_OpenAIChat,
- APIToken: apiKey,
- Endpoint: "https://api.openai.com/v1/chat/completions",
- Model: model,
- MaxTokens: 4096,
- ThinkingLevel: uctypes.ThinkingLevelMedium,
- }
-
- chatID := uuid.New().String()
-
- aiMessage := &uctypes.AIMessage{
- MessageId: uuid.New().String(),
- Parts: []uctypes.AIMessagePart{
- {
- Type: uctypes.AIMessagePartTypeText,
- Text: message,
- },
- },
- }
-
- fmt.Printf("Testing OpenAI Completions API with WaveAIPostMessageWrap, model: %s\n", model)
- fmt.Printf("Message: %s\n", message)
- fmt.Printf("Chat ID: %s\n", chatID)
- fmt.Println("---")
-
- testWriter := &TestResponseWriter{}
- sseHandler := sse.MakeSSEHandlerCh(testWriter, ctx)
- defer sseHandler.Close()
-
- chatOpts := uctypes.WaveChatOpts{
- ChatId: chatID,
- ClientId: uuid.New().String(),
- Config: *opts,
- Tools: tools,
- SystemPrompt: []string{"You are a helpful assistant. Be concise and clear in your responses."},
- }
- err := aiusechat.WaveAIPostMessageWrap(ctx, sseHandler, aiMessage, chatOpts)
- if err != nil {
- fmt.Printf("OpenAI Completions API streaming error: %v\n", err)
- }
-}
-
-func testOpenRouter(ctx context.Context, model, message string, tools []uctypes.ToolDefinition) {
- apiKey := os.Getenv("OPENROUTER_APIKEY")
- if apiKey == "" {
- fmt.Println("Error: OPENROUTER_APIKEY environment variable not set")
- os.Exit(1)
- }
-
- opts := &uctypes.AIOptsType{
- APIType: uctypes.APIType_OpenAIChat,
- APIToken: apiKey,
- Endpoint: "https://openrouter.ai/api/v1/chat/completions",
- Model: model,
- MaxTokens: 4096,
- ThinkingLevel: uctypes.ThinkingLevelMedium,
- }
-
- chatID := uuid.New().String()
-
- aiMessage := &uctypes.AIMessage{
- MessageId: uuid.New().String(),
- Parts: []uctypes.AIMessagePart{
- {
- Type: uctypes.AIMessagePartTypeText,
- Text: message,
- },
- },
- }
-
- fmt.Printf("Testing OpenRouter with WaveAIPostMessageWrap, model: %s\n", model)
- fmt.Printf("Message: %s\n", message)
- fmt.Printf("Chat ID: %s\n", chatID)
- fmt.Println("---")
-
- testWriter := &TestResponseWriter{}
- sseHandler := sse.MakeSSEHandlerCh(testWriter, ctx)
- defer sseHandler.Close()
-
- chatOpts := uctypes.WaveChatOpts{
- ChatId: chatID,
- ClientId: uuid.New().String(),
- Config: *opts,
- Tools: tools,
- SystemPrompt: []string{"You are a helpful assistant. Be concise and clear in your responses."},
- }
- err := aiusechat.WaveAIPostMessageWrap(ctx, sseHandler, aiMessage, chatOpts)
- if err != nil {
- fmt.Printf("OpenRouter streaming error: %v\n", err)
- }
-}
-
-func testNanoGPT(ctx context.Context, model, message string, tools []uctypes.ToolDefinition) {
- apiKey := os.Getenv("NANOGPT_KEY")
- if apiKey == "" {
- fmt.Println("Error: NANOGPT_KEY environment variable not set")
- os.Exit(1)
- }
-
- opts := &uctypes.AIOptsType{
- APIType: uctypes.APIType_OpenAIChat,
- APIToken: apiKey,
- Endpoint: "https://nano-gpt.com/api/v1/chat/completions",
- Model: model,
- MaxTokens: 4096,
- }
-
- chatID := uuid.New().String()
-
- aiMessage := &uctypes.AIMessage{
- MessageId: uuid.New().String(),
- Parts: []uctypes.AIMessagePart{
- {
- Type: uctypes.AIMessagePartTypeText,
- Text: message,
- },
- },
- }
-
- fmt.Printf("Testing NanoGPT with WaveAIPostMessageWrap, model: %s\n", model)
- fmt.Printf("Message: %s\n", message)
- fmt.Printf("Chat ID: %s\n", chatID)
- fmt.Println("---")
-
- testWriter := &TestResponseWriter{}
- sseHandler := sse.MakeSSEHandlerCh(testWriter, ctx)
- defer sseHandler.Close()
-
- chatOpts := uctypes.WaveChatOpts{
- ChatId: chatID,
- ClientId: uuid.New().String(),
- Config: *opts,
- Tools: tools,
- SystemPrompt: []string{"You are a helpful assistant. Be concise and clear in your responses."},
- }
- err := aiusechat.WaveAIPostMessageWrap(ctx, sseHandler, aiMessage, chatOpts)
- if err != nil {
- fmt.Printf("NanoGPT streaming error: %v\n", err)
- }
-}
-
-func testAnthropic(ctx context.Context, model, message string, tools []uctypes.ToolDefinition) {
- apiKey := os.Getenv("ANTHROPIC_APIKEY")
- if apiKey == "" {
- fmt.Println("Error: ANTHROPIC_APIKEY environment variable not set")
- os.Exit(1)
- }
-
- opts := &uctypes.AIOptsType{
- APIType: uctypes.APIType_AnthropicMessages,
- APIToken: apiKey,
- Model: model,
- MaxTokens: 4096,
- ThinkingLevel: uctypes.ThinkingLevelMedium,
- }
-
- // Generate a chat ID
- chatID := uuid.New().String()
-
- // Convert to AIMessage format for WaveAIPostMessageWrap
- aiMessage := &uctypes.AIMessage{
- MessageId: uuid.New().String(),
- Parts: []uctypes.AIMessagePart{
- {
- Type: uctypes.AIMessagePartTypeText,
- Text: message,
- },
- },
- }
-
- fmt.Printf("Testing Anthropic streaming with WaveAIPostMessageWrap, model: %s\n", model)
- fmt.Printf("Message: %s\n", message)
- fmt.Printf("Chat ID: %s\n", chatID)
- fmt.Println("---")
-
- testWriter := &TestResponseWriter{}
- sseHandler := sse.MakeSSEHandlerCh(testWriter, ctx)
- defer sseHandler.Close()
-
- chatOpts := uctypes.WaveChatOpts{
- ChatId: chatID,
- ClientId: uuid.New().String(),
- Config: *opts,
- Tools: tools,
- }
- err := aiusechat.WaveAIPostMessageWrap(ctx, sseHandler, aiMessage, chatOpts)
- if err != nil {
- fmt.Printf("Anthropic streaming error: %v\n", err)
- }
-}
-
-func testGemini(ctx context.Context, model, message string, tools []uctypes.ToolDefinition) {
- apiKey := os.Getenv("GOOGLE_APIKEY")
- if apiKey == "" {
- fmt.Println("Error: GOOGLE_APIKEY environment variable not set")
- os.Exit(1)
- }
-
- opts := &uctypes.AIOptsType{
- APIType: uctypes.APIType_GoogleGemini,
- APIToken: apiKey,
- Model: model,
- MaxTokens: 8192,
- Capabilities: []string{uctypes.AICapabilityTools, uctypes.AICapabilityImages, uctypes.AICapabilityPdfs},
- }
-
- // Generate a chat ID
- chatID := uuid.New().String()
-
- // Convert to AIMessage format for WaveAIPostMessageWrap
- aiMessage := &uctypes.AIMessage{
- MessageId: uuid.New().String(),
- Parts: []uctypes.AIMessagePart{
- {
- Type: uctypes.AIMessagePartTypeText,
- Text: message,
- },
- },
- }
-
- fmt.Printf("Testing Google Gemini streaming with WaveAIPostMessageWrap, model: %s\n", model)
- fmt.Printf("Message: %s\n", message)
- fmt.Printf("Chat ID: %s\n", chatID)
- fmt.Println("---")
-
- testWriter := &TestResponseWriter{}
- sseHandler := sse.MakeSSEHandlerCh(testWriter, ctx)
- defer sseHandler.Close()
-
- chatOpts := uctypes.WaveChatOpts{
- ChatId: chatID,
- ClientId: uuid.New().String(),
- Config: *opts,
- Tools: tools,
- SystemPrompt: []string{"You are a helpful assistant. Be concise and clear in your responses."},
- }
- err := aiusechat.WaveAIPostMessageWrap(ctx, sseHandler, aiMessage, chatOpts)
- if err != nil {
- fmt.Printf("Google Gemini streaming error: %v\n", err)
- }
-}
-
-func testT1(ctx context.Context) {
- tool := aiusechat.GetAdderToolDefinition()
- tools := []uctypes.ToolDefinition{tool}
- testAnthropic(ctx, DefaultAnthropicModel, "what is 2+2, use the provider adder tool", tools)
-}
-
-func testT2(ctx context.Context) {
- tool := aiusechat.GetAdderToolDefinition()
- tools := []uctypes.ToolDefinition{tool}
- testOpenAI(ctx, DefaultOpenAIModel, "what is 2+2+8, use the provider adder tool", tools)
-}
-
-func testT3(ctx context.Context) {
- testOpenAIComp(ctx, "gpt-4o", "what is 2+2? please be brief", nil)
-}
-
-func testT4(ctx context.Context) {
- tool := aiusechat.GetAdderToolDefinition()
- tools := []uctypes.ToolDefinition{tool}
- testGemini(ctx, DefaultGeminiModel, "what is 2+2+8, use the provider adder tool", tools)
-}
-
-func printUsage() {
- fmt.Println("Usage: go run main-testai.go [--anthropic|--openaicomp|--openrouter|--nanogpt|--gemini] [--tools] [--model ] [message]")
- fmt.Println("Examples:")
- fmt.Println(" go run main-testai.go 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --model o4-mini 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --anthropic 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --anthropic --model claude-3-5-sonnet-20241022 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --openaicomp --model gpt-4o 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --openrouter 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --openrouter --model anthropic/claude-3.5-sonnet 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --nanogpt 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --nanogpt --model gpt-4o 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --gemini 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --gemini --model gemini-1.5-pro 'What is 2+2?'")
- fmt.Println(" go run main-testai.go --tools 'Help me configure GitHub Actions monitoring'")
- fmt.Println("")
- fmt.Println("Default models:")
- fmt.Printf(" OpenAI: %s\n", DefaultOpenAIModel)
- fmt.Printf(" Anthropic: %s\n", DefaultAnthropicModel)
- fmt.Printf(" OpenAI Completions: gpt-4o\n")
- fmt.Printf(" OpenRouter: %s\n", DefaultOpenRouterModel)
- fmt.Printf(" NanoGPT: %s\n", DefaultNanoGPTModel)
- fmt.Printf(" Google Gemini: %s\n", DefaultGeminiModel)
- fmt.Println("")
- fmt.Println("Environment variables:")
- fmt.Println(" OPENAI_APIKEY (for OpenAI models)")
- fmt.Println(" ANTHROPIC_APIKEY (for Anthropic models)")
- fmt.Println(" OPENROUTER_APIKEY (for OpenRouter models)")
- fmt.Println(" NANOGPT_KEY (for NanoGPT models)")
- fmt.Println(" GOOGLE_APIKEY (for Google Gemini models)")
-}
-
-func main() {
- var anthropic, openaicomp, openrouter, nanogpt, gemini, tools, help, t1, t2, t3, t4 bool
- var model string
- flag.BoolVar(&anthropic, "anthropic", false, "Use Anthropic API instead of OpenAI")
- flag.BoolVar(&openaicomp, "openaicomp", false, "Use OpenAI Completions API")
- flag.BoolVar(&openrouter, "openrouter", false, "Use OpenRouter API")
- flag.BoolVar(&nanogpt, "nanogpt", false, "Use NanoGPT API")
- flag.BoolVar(&gemini, "gemini", false, "Use Google Gemini API")
- flag.BoolVar(&tools, "tools", false, "Enable GitHub Actions Monitor tools for testing")
- flag.StringVar(&model, "model", "", fmt.Sprintf("AI model to use (defaults: %s for OpenAI, %s for Anthropic, %s for OpenRouter, %s for NanoGPT, %s for Gemini)", DefaultOpenAIModel, DefaultAnthropicModel, DefaultOpenRouterModel, DefaultNanoGPTModel, DefaultGeminiModel))
- flag.BoolVar(&help, "help", false, "Show usage information")
- flag.BoolVar(&t1, "t1", false, fmt.Sprintf("Run preset T1 test (%s with 'what is 2+2')", DefaultAnthropicModel))
- flag.BoolVar(&t2, "t2", false, fmt.Sprintf("Run preset T2 test (%s with 'what is 2+2')", DefaultOpenAIModel))
- flag.BoolVar(&t3, "t3", false, "Run preset T3 test (OpenAI Completions API with gpt-5.1)")
- flag.BoolVar(&t4, "t4", false, "Run preset T4 test (OpenAI Completions API with gemini-3-pro-preview)")
- flag.Parse()
-
- if help {
- printUsage()
- os.Exit(0)
- }
-
- ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
- defer cancel()
-
- if t1 {
- testT1(ctx)
- return
- }
- if t2 {
- testT2(ctx)
- return
- }
- if t3 {
- testT3(ctx)
- return
- }
- if t4 {
- testT4(ctx)
- return
- }
-
- // Set default model based on API type if not provided
- if model == "" {
- if anthropic {
- model = DefaultAnthropicModel
- } else if openaicomp {
- model = "gpt-4o"
- } else if openrouter {
- model = DefaultOpenRouterModel
- } else if nanogpt {
- model = DefaultNanoGPTModel
- } else if gemini {
- model = DefaultGeminiModel
- } else {
- model = DefaultOpenAIModel
- }
- }
-
- args := flag.Args()
- message := "What is 2+2?"
- if len(args) > 0 {
- message = args[0]
- }
-
- var toolDefs []uctypes.ToolDefinition
- if tools {
- toolDefs = getToolDefinitions()
- }
-
- if anthropic {
- testAnthropic(ctx, model, message, toolDefs)
- } else if openaicomp {
- testOpenAIComp(ctx, model, message, toolDefs)
- } else if openrouter {
- testOpenRouter(ctx, model, message, toolDefs)
- } else if nanogpt {
- testNanoGPT(ctx, model, message, toolDefs)
- } else if gemini {
- testGemini(ctx, model, message, toolDefs)
- } else {
- testOpenAI(ctx, model, message, toolDefs)
- }
-}
diff --git a/cmd/testai/testschema.json b/cmd/testai/testschema.json
deleted file mode 100644
index dc9de2b834..0000000000
--- a/cmd/testai/testschema.json
+++ /dev/null
@@ -1,104 +0,0 @@
-{
- "config": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "description": "Application configuration settings",
- "properties": {
- "maxWorkflowRuns": {
- "description": "Maximum number of workflow runs to fetch",
- "maximum": 100,
- "minimum": 1,
- "type": "integer"
- },
- "pollInterval": {
- "description": "Polling interval for GitHub API requests",
- "maximum": 300,
- "minimum": 1,
- "type": "integer",
- "units": "s"
- },
- "repository": {
- "description": "GitHub repository in owner/repo format",
- "pattern": "^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$",
- "type": "string"
- },
- "workflow": {
- "description": "GitHub Actions workflow file name",
- "pattern": "^.+\\.(yml|yaml)$",
- "type": "string"
- }
- },
- "title": "Application Configuration",
- "type": "object"
- },
- "data": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "definitions": {
- "WorkflowRun": {
- "properties": {
- "conclusion": {
- "type": "string"
- },
- "created_at": {
- "format": "date-time",
- "type": "string"
- },
- "html_url": {
- "type": "string"
- },
- "id": {
- "type": "integer"
- },
- "name": {
- "type": "string"
- },
- "run_number": {
- "type": "integer"
- },
- "status": {
- "type": "string"
- },
- "updated_at": {
- "format": "date-time",
- "type": "string"
- }
- },
- "required": [
- "id",
- "name",
- "status",
- "conclusion",
- "created_at",
- "updated_at",
- "html_url",
- "run_number"
- ],
- "type": "object"
- }
- },
- "description": "Application data schema",
- "properties": {
- "isLoading": {
- "description": "Loading state for workflow data fetch",
- "type": "boolean"
- },
- "lastError": {
- "description": "Last error message from GitHub API",
- "type": "string"
- },
- "lastRefreshTime": {
- "description": "Timestamp of last successful data refresh",
- "format": "date-time",
- "type": "string"
- },
- "workflowRuns": {
- "description": "List of GitHub Actions workflow runs",
- "items": {
- "$ref": "#/definitions/WorkflowRun"
- },
- "type": "array"
- }
- },
- "title": "Application Data",
- "type": "object"
- }
-}
diff --git a/cmd/testopenai/main-testopenai.go b/cmd/testopenai/main-testopenai.go
deleted file mode 100644
index 7017407b47..0000000000
--- a/cmd/testopenai/main-testopenai.go
+++ /dev/null
@@ -1,164 +0,0 @@
-// Copyright 2025, Command Line Inc.
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import (
- "bufio"
- "bytes"
- "context"
- "encoding/json"
- "flag"
- "fmt"
- "io"
- "net/http"
- "os"
- "time"
-
- "github.com/wavetermdev/waveterm/pkg/aiusechat"
- "github.com/wavetermdev/waveterm/pkg/aiusechat/openai"
-)
-
-func makeOpenAIRequest(ctx context.Context, apiKey, model, message string, tools bool) error {
- reqBody := openai.OpenAIRequest{
- Model: model,
- Input: []any{
- openai.OpenAIMessage{
- Role: "user",
- Content: []openai.OpenAIMessageContent{
- {
- Type: "input_text",
- Text: message,
- },
- },
- },
- },
- Stream: true,
- StreamOptions: &openai.StreamOptionsType{IncludeObfuscation: false},
- Reasoning: &openai.ReasoningType{Effort: "medium"},
- }
- if tools {
- reqBody.Tools = []openai.OpenAIRequestTool{
- openai.ConvertToolDefinitionToOpenAI(aiusechat.GetAdderToolDefinition()),
- }
- }
-
- jsonData, err := json.Marshal(reqBody)
- if err != nil {
- return fmt.Errorf("error marshaling request: %v", err)
- }
-
- // Pretty print the request JSON for debugging
- prettyJSON, err := json.MarshalIndent(reqBody, "", " ")
- if err == nil {
- fmt.Printf("Request JSON:\n%s\n", string(prettyJSON))
- }
-
- req, err := http.NewRequestWithContext(ctx, "POST", "https://api.openai.com/v1/responses", bytes.NewBuffer(jsonData))
- if err != nil {
- return fmt.Errorf("error creating request: %v", err)
- }
-
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", "Bearer "+apiKey)
- req.Header.Set("Accept", "text/event-stream")
-
- client := &http.Client{
- Timeout: 60 * time.Second,
- }
-
- resp, err := client.Do(req)
- if err != nil {
- return fmt.Errorf("error making request: %v", err)
- }
- defer resp.Body.Close()
-
- fmt.Printf("Response Status: %s\n", resp.Status)
- fmt.Printf("Response Headers:\n")
- for name, values := range resp.Header {
- for _, value := range values {
- fmt.Printf(" %s: %s\n", name, value)
- }
- }
- fmt.Println("---")
-
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- return fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
- }
-
- return processSSEStream(resp.Body)
-}
-
-func processSSEStream(reader io.Reader) error {
- scanner := bufio.NewScanner(reader)
-
- fmt.Println("SSE Stream:")
- fmt.Println("---")
-
- for scanner.Scan() {
- line := scanner.Text()
- fmt.Println(line)
- }
-
- if err := scanner.Err(); err != nil {
- return fmt.Errorf("error reading stream: %v", err)
- }
-
- return nil
-}
-
-func printUsage() {
- fmt.Println("Usage: go run main-testopenai.go [--model ] [--tools] [message]")
- fmt.Println("Examples:")
- fmt.Println(" go run main-testopenai.go 'Stream me a limerick about gophers coding in Go.'")
- fmt.Println(" go run main-testopenai.go --model gpt-4 'What is 2+2?'")
- fmt.Println(" go run main-testopenai.go --tools 'What is 2+2? Use the adder tool.'")
- fmt.Println("")
- fmt.Println("Default model: gpt-5-mini")
- fmt.Println("")
- fmt.Println("Environment variables:")
- fmt.Println(" OPENAI_APIKEY (required)")
-}
-
-func main() {
- var model string
- var showHelp bool
- var tools bool
-
- flag.StringVar(&model, "model", "gpt-5-mini", "OpenAI model to use")
- flag.BoolVar(&showHelp, "help", false, "Show usage information")
- flag.BoolVar(&tools, "tools", false, "Enable tools for testing")
- flag.Parse()
-
- if showHelp {
- printUsage()
- os.Exit(0)
- }
-
- apiKey := os.Getenv("OPENAI_APIKEY")
- if apiKey == "" {
- fmt.Println("Error: OPENAI_APIKEY environment variable not set")
- printUsage()
- os.Exit(1)
- }
-
- args := flag.Args()
- message := "Stream me a limerick about gophers coding in Go."
- if len(args) > 0 {
- message = args[0]
- }
-
- ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
- defer cancel()
-
- fmt.Printf("Testing OpenAI Responses API\n")
- fmt.Printf("Model: %s\n", model)
- fmt.Printf("Message: %s\n", message)
- fmt.Println("===")
-
- if err := makeOpenAIRequest(ctx, apiKey, model, message, tools); err != nil {
- fmt.Printf("Error: %v\n", err)
- os.Exit(1)
- }
-}
diff --git a/cmd/testsummarize/main-testsummarize.go b/cmd/testsummarize/main-testsummarize.go
deleted file mode 100644
index fc16e59e04..0000000000
--- a/cmd/testsummarize/main-testsummarize.go
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2025, Command Line Inc.
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import (
- "context"
- "flag"
- "fmt"
- "os"
- "time"
-
- "github.com/wavetermdev/waveterm/pkg/aiusechat/google"
-)
-
-func printUsage() {
- fmt.Println("Usage: go run main-testsummarize.go [--help] [--mode MODE] ")
- fmt.Println("Examples:")
- fmt.Println(" go run main-testsummarize.go README.md")
- fmt.Println(" go run main-testsummarize.go --mode useful /path/to/image.png")
- fmt.Println(" go run main-testsummarize.go -m publiccode document.pdf")
- fmt.Println("")
- fmt.Println("Supported file types:")
- fmt.Println(" - Text files (up to 200KB)")
- fmt.Println(" - Images (up to 7MB)")
- fmt.Println(" - PDFs (up to 5MB)")
- fmt.Println("")
- fmt.Println("Flags:")
- fmt.Println(" --mode, -m Summarization mode (default: quick)")
- fmt.Println(" Options: quick, useful, publiccode, htmlcontent, htmlfull")
- fmt.Println("")
- fmt.Println("Environment variables:")
- fmt.Println(" GOOGLE_APIKEY (required)")
-}
-
-func main() {
- var showHelp bool
- var mode string
- flag.BoolVar(&showHelp, "help", false, "Show usage information")
- flag.StringVar(&mode, "mode", "quick", "Summarization mode")
- flag.StringVar(&mode, "m", "quick", "Summarization mode (shorthand)")
- flag.Parse()
-
- if showHelp {
- printUsage()
- os.Exit(0)
- }
-
- apiKey := os.Getenv("GOOGLE_APIKEY")
- if apiKey == "" {
- fmt.Println("Error: GOOGLE_APIKEY environment variable not set")
- printUsage()
- os.Exit(1)
- }
-
- args := flag.Args()
- if len(args) == 0 {
- fmt.Println("Error: filename required")
- printUsage()
- os.Exit(1)
- }
-
- filename := args[0]
-
- // Check if file exists
- if _, err := os.Stat(filename); os.IsNotExist(err) {
- fmt.Printf("Error: file '%s' does not exist\n", filename)
- os.Exit(1)
- }
-
- ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
- defer cancel()
-
- fmt.Printf("Summarizing file: %s\n", filename)
- fmt.Printf("Model: %s\n", google.SummarizeModel)
- fmt.Printf("Mode: %s\n", mode)
-
- startTime := time.Now()
- summary, usage, err := google.SummarizeFile(ctx, filename, google.SummarizeOpts{
- APIKey: apiKey,
- Mode: mode,
- })
- latency := time.Since(startTime)
-
- fmt.Printf("Latency: %d ms\n", latency.Milliseconds())
- fmt.Println("===")
- if err != nil {
- fmt.Printf("Error: %v\n", err)
- os.Exit(1)
- }
-
- fmt.Println("\nSummary:")
- fmt.Println("---")
- fmt.Println(summary)
- fmt.Println("---")
-
- if usage != nil {
- fmt.Println("\nUsage Statistics:")
- fmt.Printf(" Prompt tokens: %d\n", usage.PromptTokenCount)
- fmt.Printf(" Cached tokens: %d\n", usage.CachedContentTokenCount)
- fmt.Printf(" Response tokens: %d\n", usage.CandidatesTokenCount)
- fmt.Printf(" Total tokens: %d\n", usage.TotalTokenCount)
- }
-}
\ No newline at end of file
diff --git a/cmd/wsh/cmd/wshcmd-ai.go b/cmd/wsh/cmd/wshcmd-ai.go
deleted file mode 100644
index 643c80ee7a..0000000000
--- a/cmd/wsh/cmd/wshcmd-ai.go
+++ /dev/null
@@ -1,211 +0,0 @@
-// Copyright 2025, Command Line Inc.
-// SPDX-License-Identifier: Apache-2.0
-
-package cmd
-
-import (
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/spf13/cobra"
- "github.com/wavetermdev/waveterm/pkg/util/fileutil"
- "github.com/wavetermdev/waveterm/pkg/util/utilfn"
- "github.com/wavetermdev/waveterm/pkg/wshrpc"
- "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
- "github.com/wavetermdev/waveterm/pkg/wshutil"
-)
-
-var aiCmd = &cobra.Command{
- Use: "ai [options] [files...]",
- Short: "Append content to Wave AI sidebar prompt",
- Long: `Append content to Wave AI sidebar prompt (does not auto-submit by default)
-
-Arguments:
- files... Files to attach (use '-' for stdin)
-
-Examples:
- git diff | wsh ai - # Pipe diff to AI, ask question in UI
- wsh ai main.go # Attach file, ask question in UI
- wsh ai *.go -m "find bugs" # Attach files with message
- wsh ai -s - -m "review" < log.txt # Stdin + message, auto-submit
- wsh ai -n config.json # New chat with file attached`,
- RunE: aiRun,
- PreRunE: preRunSetupRpcClient,
- DisableFlagsInUseLine: true,
-}
-
-var aiMessageFlag string
-var aiSubmitFlag bool
-var aiNewBlockFlag bool
-
-func init() {
- rootCmd.AddCommand(aiCmd)
- aiCmd.Flags().StringVarP(&aiMessageFlag, "message", "m", "", "optional message/question to append after files")
- aiCmd.Flags().BoolVarP(&aiSubmitFlag, "submit", "s", false, "submit the prompt immediately after appending")
- aiCmd.Flags().BoolVarP(&aiNewBlockFlag, "new", "n", false, "create a new AI chat instead of using existing")
-}
-
-func detectMimeType(data []byte) string {
- mimeType := http.DetectContentType(data)
- return strings.Split(mimeType, ";")[0]
-}
-
-func getMaxFileSize(mimeType string) (int, string) {
- if mimeType == "application/pdf" {
- return 5 * 1024 * 1024, "5MB"
- }
- if strings.HasPrefix(mimeType, "image/") {
- return 7 * 1024 * 1024, "7MB"
- }
- return 200 * 1024, "200KB"
-}
-
-func aiRun(cmd *cobra.Command, args []string) (rtnErr error) {
- defer func() {
- sendActivity("ai", rtnErr == nil)
- }()
-
- if len(args) == 0 && aiMessageFlag == "" {
- OutputHelpMessage(cmd)
- return fmt.Errorf("no files or message provided")
- }
-
- const maxFileCount = 15
- const rpcTimeout = 30000
-
- var allFiles []wshrpc.AIAttachedFile
- var stdinUsed bool
-
- if len(args) > maxFileCount {
- return fmt.Errorf("too many files (maximum %d files allowed)", maxFileCount)
- }
-
- for _, filePath := range args {
- var data []byte
- var fileName string
- var mimeType string
- var err error
-
- if filePath == "-" {
- if stdinUsed {
- return fmt.Errorf("stdin (-) can only be used once")
- }
- stdinUsed = true
-
- data, err = io.ReadAll(os.Stdin)
- if err != nil {
- return fmt.Errorf("reading from stdin: %w", err)
- }
- fileName = "stdin"
- mimeType = "text/plain"
- } else {
- fileInfo, err := os.Stat(filePath)
- if err != nil {
- return fmt.Errorf("accessing file %s: %w", filePath, err)
- }
- absPath, err := filepath.Abs(filePath)
- if err != nil {
- return fmt.Errorf("getting absolute path for %s: %w", filePath, err)
- }
-
- if fileInfo.IsDir() {
- result, err := fileutil.ReadDir(filePath, 500)
- if err != nil {
- return fmt.Errorf("reading directory %s: %w", filePath, err)
- }
- jsonData, err := json.Marshal(result)
- if err != nil {
- return fmt.Errorf("marshaling directory listing for %s: %w", filePath, err)
- }
- data = jsonData
- fileName = absPath
- mimeType = "directory"
- } else {
- data, err = os.ReadFile(filePath)
- if err != nil {
- return fmt.Errorf("reading file %s: %w", filePath, err)
- }
- fileName = absPath
- mimeType = detectMimeType(data)
- }
- }
-
- isPDF := mimeType == "application/pdf"
- isImage := strings.HasPrefix(mimeType, "image/")
- isDirectory := mimeType == "directory"
-
- if !isPDF && !isImage && !isDirectory {
- mimeType = "text/plain"
- if utilfn.ContainsBinaryData(data) {
- return fmt.Errorf("file %s contains binary data and cannot be uploaded as text", fileName)
- }
- }
-
- maxSize, sizeStr := getMaxFileSize(mimeType)
- if len(data) > maxSize {
- return fmt.Errorf("file %s exceeds maximum size of %s for %s files", fileName, sizeStr, mimeType)
- }
-
- allFiles = append(allFiles, wshrpc.AIAttachedFile{
- Name: fileName,
- Type: mimeType,
- Size: len(data),
- Data64: base64.StdEncoding.EncodeToString(data),
- })
- }
-
- tabId := os.Getenv("WAVETERM_TABID")
- if tabId == "" {
- return fmt.Errorf("WAVETERM_TABID environment variable not set")
- }
-
- route := wshutil.MakeTabRouteId(tabId)
-
- if aiNewBlockFlag {
- newChatData := wshrpc.CommandWaveAIAddContextData{
- NewChat: true,
- }
- err := wshclient.WaveAIAddContextCommand(RpcClient, newChatData, &wshrpc.RpcOpts{
- Route: route,
- Timeout: rpcTimeout,
- })
- if err != nil {
- return fmt.Errorf("creating new chat: %w", err)
- }
- }
-
- for _, file := range allFiles {
- contextData := wshrpc.CommandWaveAIAddContextData{
- Files: []wshrpc.AIAttachedFile{file},
- }
- err := wshclient.WaveAIAddContextCommand(RpcClient, contextData, &wshrpc.RpcOpts{
- Route: route,
- Timeout: rpcTimeout,
- })
- if err != nil {
- return fmt.Errorf("adding file %s: %w", file.Name, err)
- }
- }
-
- if aiMessageFlag != "" || aiSubmitFlag {
- finalContextData := wshrpc.CommandWaveAIAddContextData{
- Text: aiMessageFlag,
- Submit: aiSubmitFlag,
- }
- err := wshclient.WaveAIAddContextCommand(RpcClient, finalContextData, &wshrpc.RpcOpts{
- Route: route,
- Timeout: rpcTimeout,
- })
- if err != nil {
- return fmt.Errorf("adding context: %w", err)
- }
- }
-
- return nil
-}
diff --git a/cmd/wsh/cmd/wshcmd-badge.go b/cmd/wsh/cmd/wshcmd-badge.go
index 590ed1e40b..f2b17a0f24 100644
--- a/cmd/wsh/cmd/wshcmd-badge.go
+++ b/cmd/wsh/cmd/wshcmd-badge.go
@@ -44,7 +44,6 @@ func init() {
func badgeRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("badge", rtnErr == nil)
}()
if badgePid > 0 && runtime.GOOS == "windows" {
diff --git a/cmd/wsh/cmd/wshcmd-blocks.go b/cmd/wsh/cmd/wshcmd-blocks.go
index 7e4b935ee3..8c231ae9c3 100644
--- a/cmd/wsh/cmd/wshcmd-blocks.go
+++ b/cmd/wsh/cmd/wshcmd-blocks.go
@@ -31,7 +31,7 @@ type BlockDetails struct {
BlockId string `json:"blockid"` // Unique identifier for the block
WorkspaceId string `json:"workspaceid"` // ID of the workspace containing the block
TabId string `json:"tabid"` // ID of the tab containing the block
- View string `json:"view"` // Canonical view type (term, web, preview, edit, sysinfo, waveai)
+ View string `json:"view"` // Canonical view type (term, web, preview, edit, sysinfo)
Meta waveobj.MetaMapType `json:"meta"` // Block metadata including view type
}
@@ -74,7 +74,7 @@ func init() {
blocksListCmd.Flags().StringVar(&blocksWindowId, "window", "", "restrict to window id")
blocksListCmd.Flags().StringVar(&blocksWorkspaceId, "workspace", "", "restrict to workspace id")
blocksListCmd.Flags().StringVar(&blocksTabId, "tab", "", "restrict to specific tab id")
- blocksListCmd.Flags().StringVar(&blocksView, "view", "", "restrict to view type (term/terminal, web/browser, preview/edit, sysinfo, waveai)")
+ blocksListCmd.Flags().StringVar(&blocksView, "view", "", "restrict to view type (term/terminal, web/browser, preview/edit, sysinfo)")
blocksListCmd.Flags().BoolVar(&blocksJSON, "json", false, "output as JSON")
blocksListCmd.Flags().IntVar(&blocksTimeout, "timeout", 5000, "timeout in milliseconds for RPC calls (default: 5000)")
@@ -100,7 +100,7 @@ func init() {
func blocksListRun(cmd *cobra.Command, args []string) error {
if v := strings.TrimSpace(blocksView); v != "" {
if !isKnownViewFilter(v) {
- return fmt.Errorf("unknown --view %q; try one of: term, web, preview, edit, sysinfo, waveai", v)
+ return fmt.Errorf("unknown --view %q; try one of: term, web, preview, edit, sysinfo", v)
}
}
@@ -270,8 +270,6 @@ func matchesViewType(actual, filter string) bool {
return strings.EqualFold(actual, "term")
case "web", "browser", "url":
return strings.EqualFold(actual, "web")
- case "ai", "waveai", "assistant":
- return strings.EqualFold(actual, "waveai")
case "sys", "sysinfo", "system":
return strings.EqualFold(actual, "sysinfo")
}
@@ -285,8 +283,7 @@ func isKnownViewFilter(f string) bool {
case "term", "terminal", "shell", "console",
"web", "browser", "url",
"preview", "edit",
- "sysinfo", "sys", "system",
- "waveai", "ai", "assistant":
+ "sysinfo", "sys", "system":
return true
default:
return false
diff --git a/cmd/wsh/cmd/wshcmd-debug.go b/cmd/wsh/cmd/wshcmd-debug.go
index 9efac0ff87..29a1857689 100644
--- a/cmd/wsh/cmd/wshcmd-debug.go
+++ b/cmd/wsh/cmd/wshcmd-debug.go
@@ -24,24 +24,11 @@ var debugBlockIdsCmd = &cobra.Command{
Hidden: true,
}
-var debugSendTelemetryCmd = &cobra.Command{
- Use: "send-telemetry",
- Short: "send telemetry",
- RunE: debugSendTelemetryRun,
- Hidden: true,
-}
-
func init() {
debugCmd.AddCommand(debugBlockIdsCmd)
- debugCmd.AddCommand(debugSendTelemetryCmd)
rootCmd.AddCommand(debugCmd)
}
-func debugSendTelemetryRun(cmd *cobra.Command, args []string) error {
- err := wshclient.SendTelemetryCommand(RpcClient, nil)
- return err
-}
-
func debugBlockIdsRun(cmd *cobra.Command, args []string) error {
oref, err := resolveBlockArg()
if err != nil {
diff --git a/cmd/wsh/cmd/wshcmd-debugterm.go b/cmd/wsh/cmd/wshcmd-debugterm.go
index 66346c460a..dabea2540e 100644
--- a/cmd/wsh/cmd/wshcmd-debugterm.go
+++ b/cmd/wsh/cmd/wshcmd-debugterm.go
@@ -50,7 +50,6 @@ func init() {
func debugTermRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("debugterm", rtnErr == nil)
}()
mode, err := getDebugTermMode()
if err != nil {
diff --git a/cmd/wsh/cmd/wshcmd-deleteblock.go b/cmd/wsh/cmd/wshcmd-deleteblock.go
index 76518e721c..c6406f928e 100644
--- a/cmd/wsh/cmd/wshcmd-deleteblock.go
+++ b/cmd/wsh/cmd/wshcmd-deleteblock.go
@@ -24,7 +24,6 @@ func init() {
func deleteBlockRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("deleteblock", rtnErr == nil)
}()
fullORef, err := resolveBlockArg()
if err != nil {
diff --git a/cmd/wsh/cmd/wshcmd-editconfig.go b/cmd/wsh/cmd/wshcmd-editconfig.go
index cbd4015bae..cedba81ad0 100644
--- a/cmd/wsh/cmd/wshcmd-editconfig.go
+++ b/cmd/wsh/cmd/wshcmd-editconfig.go
@@ -30,7 +30,6 @@ func init() {
func editConfigRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("editconfig", rtnErr == nil)
}()
configFile := "settings.json" // default
diff --git a/cmd/wsh/cmd/wshcmd-editor.go b/cmd/wsh/cmd/wshcmd-editor.go
index 4968b17509..9459dcc065 100644
--- a/cmd/wsh/cmd/wshcmd-editor.go
+++ b/cmd/wsh/cmd/wshcmd-editor.go
@@ -32,7 +32,6 @@ func init() {
func editorRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("editor", rtnErr == nil)
}()
if len(args) == 0 {
OutputHelpMessage(cmd)
diff --git a/cmd/wsh/cmd/wshcmd-file.go b/cmd/wsh/cmd/wshcmd-file.go
index e40eb324d2..7c668ef2dd 100644
--- a/cmd/wsh/cmd/wshcmd-file.go
+++ b/cmd/wsh/cmd/wshcmd-file.go
@@ -90,7 +90,7 @@ var fileListCmd = &cobra.Command{
Short: "list files",
Long: "List files in a directory. By default, lists files in the current directory." + UriHelpText,
Example: " wsh file ls wsh://user@ec2/home/user/",
- RunE: activityWrap("file", fileListRun),
+ RunE: fileListRun,
PreRunE: preRunSetupRpcClient,
}
@@ -100,7 +100,7 @@ var fileCatCmd = &cobra.Command{
Long: "Display the contents of a file." + UriHelpText,
Example: " wsh file cat wsh://user@ec2/home/user/config.txt",
Args: cobra.ExactArgs(1),
- RunE: activityWrap("file", fileCatRun),
+ RunE: fileCatRun,
PreRunE: preRunSetupRpcClient,
}
@@ -110,7 +110,7 @@ var fileInfoCmd = &cobra.Command{
Long: "Show information about a file." + UriHelpText,
Example: " wsh file info wsh://user@ec2/home/user/config.txt",
Args: cobra.ExactArgs(1),
- RunE: activityWrap("file", fileInfoRun),
+ RunE: fileInfoRun,
PreRunE: preRunSetupRpcClient,
}
@@ -120,7 +120,7 @@ var fileRmCmd = &cobra.Command{
Long: "Remove a file." + UriHelpText,
Example: " wsh file rm wsh://user@ec2/home/user/config.txt",
Args: cobra.ExactArgs(1),
- RunE: activityWrap("file", fileRmRun),
+ RunE: fileRmRun,
PreRunE: preRunSetupRpcClient,
}
@@ -130,7 +130,7 @@ var fileWriteCmd = &cobra.Command{
Long: "Write stdin into a file, buffering input (10MB total file size limit)." + UriHelpText,
Example: " echo 'hello' | wsh file write ./greeting.txt",
Args: cobra.ExactArgs(1),
- RunE: activityWrap("file", fileWriteRun),
+ RunE: fileWriteRun,
PreRunE: preRunSetupRpcClient,
}
@@ -140,7 +140,7 @@ var fileAppendCmd = &cobra.Command{
Long: "Append stdin to a file, buffering input (10MB total file size limit)." + UriHelpText,
Example: " tail -f log.txt | wsh file append ./app.log",
Args: cobra.ExactArgs(1),
- RunE: activityWrap("file", fileAppendRun),
+ RunE: fileAppendRun,
PreRunE: preRunSetupRpcClient,
}
@@ -151,7 +151,7 @@ var fileCpCmd = &cobra.Command{
Long: "Copy files between different storage systems." + UriHelpText,
Example: " wsh file cp wsh://user@ec2/home/user/config.txt ./local-config.txt\n wsh file cp ./local-config.txt wsh://user@ec2/home/user/config.txt",
Args: cobra.ExactArgs(2),
- RunE: activityWrap("file", fileCpRun),
+ RunE: fileCpRun,
PreRunE: preRunSetupRpcClient,
}
@@ -162,7 +162,7 @@ var fileMvCmd = &cobra.Command{
Long: "Move files between different storage systems. The source file will be deleted once the operation completes successfully." + UriHelpText,
Example: " wsh file mv wsh://user@ec2/home/user/config.txt ./local-config.txt\n wsh file mv ./local-config.txt wsh://user@ec2/home/user/config.txt",
Args: cobra.ExactArgs(2),
- RunE: activityWrap("file", fileMvRun),
+ RunE: fileMvRun,
PreRunE: preRunSetupRpcClient,
}
diff --git a/cmd/wsh/cmd/wshcmd-focusblock.go b/cmd/wsh/cmd/wshcmd-focusblock.go
index 3f6603a3e2..ff5f224d9d 100644
--- a/cmd/wsh/cmd/wshcmd-focusblock.go
+++ b/cmd/wsh/cmd/wshcmd-focusblock.go
@@ -26,7 +26,6 @@ func init() {
func focusBlockRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("focusblock", rtnErr == nil)
}()
tabId := os.Getenv("WAVETERM_TABID")
diff --git a/cmd/wsh/cmd/wshcmd-getmeta.go b/cmd/wsh/cmd/wshcmd-getmeta.go
index f5e1e40f67..b713bbef4f 100644
--- a/cmd/wsh/cmd/wshcmd-getmeta.go
+++ b/cmd/wsh/cmd/wshcmd-getmeta.go
@@ -74,7 +74,6 @@ func filterMetaKeys(meta map[string]interface{}, keys []string) map[string]inter
func getMetaRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("getmeta", rtnErr == nil)
}()
fullORef, err := resolveBlockArg()
if err != nil {
diff --git a/cmd/wsh/cmd/wshcmd-getvar.go b/cmd/wsh/cmd/wshcmd-getvar.go
index 9391c4f5f2..1a42be43a3 100644
--- a/cmd/wsh/cmd/wshcmd-getvar.go
+++ b/cmd/wsh/cmd/wshcmd-getvar.go
@@ -53,7 +53,6 @@ func shouldPrintNewline() bool {
func getVarRun(cmd *cobra.Command, args []string) error {
defer func() {
- sendActivity("getvar", WshExitCode == 0)
}()
// Resolve block to get zoneId
diff --git a/cmd/wsh/cmd/wshcmd-launch.go b/cmd/wsh/cmd/wshcmd-launch.go
index 3ec582a6cd..d7d29385e1 100644
--- a/cmd/wsh/cmd/wshcmd-launch.go
+++ b/cmd/wsh/cmd/wshcmd-launch.go
@@ -28,7 +28,6 @@ func init() {
func launchRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("launch", rtnErr == nil)
}()
widgetId := args[0]
diff --git a/cmd/wsh/cmd/wshcmd-notify.go b/cmd/wsh/cmd/wshcmd-notify.go
index de2086e1f7..a73f2b9551 100644
--- a/cmd/wsh/cmd/wshcmd-notify.go
+++ b/cmd/wsh/cmd/wshcmd-notify.go
@@ -31,7 +31,6 @@ func init() {
func notifyRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("notify", rtnErr == nil)
}()
message := args[0]
notificationOptions := &wshrpc.WaveNotificationOptions{
diff --git a/cmd/wsh/cmd/wshcmd-root.go b/cmd/wsh/cmd/wshcmd-root.go
index 9534d2e5f5..a477f4c5a4 100644
--- a/cmd/wsh/cmd/wshcmd-root.go
+++ b/cmd/wsh/cmd/wshcmd-root.go
@@ -103,15 +103,6 @@ func getIsTty() bool {
type RunEFnType = func(*cobra.Command, []string) error
-func activityWrap(activityStr string, origRunE RunEFnType) RunEFnType {
- return func(cmd *cobra.Command, args []string) (rtnErr error) {
- defer func() {
- sendActivity(activityStr, rtnErr == nil)
- }()
- return origRunE(cmd, args)
- }
-}
-
func resolveBlockArg() (*waveobj.ORef, error) {
oref := blockArg
if oref == "" {
@@ -213,23 +204,6 @@ func getTabIdFromEnv() string {
return os.Getenv("WAVETERM_TABID")
}
-// this will send wsh activity to the client running on *your* local machine (it does not contact any wave cloud infrastructure)
-// if you've turned off telemetry in your local client, this data never gets sent to us
-// no parameters or timestamps are sent, as you can see below, it just sends the name of the command (and if there was an error)
-// (e.g. "wsh ai ..." would send "ai")
-// this helps us understand which commands are actually being used so we know where to concentrate our effort
-func sendActivity(wshCmdName string, success bool) {
- if RpcClient == nil || wshCmdName == "" {
- return
- }
- dataMap := make(map[string]int)
- dataMap[wshCmdName] = 1
- if !success {
- dataMap[wshCmdName+"#"+"error"] = 1
- }
- wshclient.WshActivityCommand(RpcClient, dataMap, nil)
-}
-
// Execute executes the root command.
func Execute() {
defer func() {
diff --git a/cmd/wsh/cmd/wshcmd-run.go b/cmd/wsh/cmd/wshcmd-run.go
index 6faf424c99..dffa24f515 100644
--- a/cmd/wsh/cmd/wshcmd-run.go
+++ b/cmd/wsh/cmd/wshcmd-run.go
@@ -40,7 +40,6 @@ func init() {
func runRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("run", rtnErr == nil)
}()
flags := cmd.Flags()
diff --git a/cmd/wsh/cmd/wshcmd-secret.go b/cmd/wsh/cmd/wshcmd-secret.go
index 916e3ae4a5..cbb45c45a9 100644
--- a/cmd/wsh/cmd/wshcmd-secret.go
+++ b/cmd/wsh/cmd/wshcmd-secret.go
@@ -77,7 +77,6 @@ func init() {
func secretGetRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("secret", rtnErr == nil)
}()
name := args[0]
@@ -101,7 +100,6 @@ func secretGetRun(cmd *cobra.Command, args []string) (rtnErr error) {
func secretSetRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("secret", rtnErr == nil)
}()
parts := strings.SplitN(args[0], "=", 2)
@@ -137,7 +135,6 @@ func secretSetRun(cmd *cobra.Command, args []string) (rtnErr error) {
func secretListRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("secret", rtnErr == nil)
}()
names, err := wshclient.GetSecretsNamesCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 2000})
@@ -153,7 +150,6 @@ func secretListRun(cmd *cobra.Command, args []string) (rtnErr error) {
func secretDeleteRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("secret", rtnErr == nil)
}()
name := args[0]
@@ -173,7 +169,6 @@ func secretDeleteRun(cmd *cobra.Command, args []string) (rtnErr error) {
func secretUiRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("secret", rtnErr == nil)
}()
tabId := getTabIdFromEnv()
diff --git a/cmd/wsh/cmd/wshcmd-setbg.go b/cmd/wsh/cmd/wshcmd-setbg.go
index 4385409187..68215a8c17 100644
--- a/cmd/wsh/cmd/wshcmd-setbg.go
+++ b/cmd/wsh/cmd/wshcmd-setbg.go
@@ -90,7 +90,6 @@ func validateColor(color string) error {
func setBgRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("setbg", rtnErr == nil)
}()
borderColorChanged := cmd.Flags().Changed("border-color")
diff --git a/cmd/wsh/cmd/wshcmd-setconfig.go b/cmd/wsh/cmd/wshcmd-setconfig.go
index 3fcd1f94b2..6ce1b5297c 100644
--- a/cmd/wsh/cmd/wshcmd-setconfig.go
+++ b/cmd/wsh/cmd/wshcmd-setconfig.go
@@ -25,7 +25,6 @@ func init() {
func setConfigRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("setconfig", rtnErr == nil)
}()
metaSetsStrs := args[:]
diff --git a/cmd/wsh/cmd/wshcmd-setmeta.go b/cmd/wsh/cmd/wshcmd-setmeta.go
index 79faa7e78c..2f2524e46f 100644
--- a/cmd/wsh/cmd/wshcmd-setmeta.go
+++ b/cmd/wsh/cmd/wshcmd-setmeta.go
@@ -158,7 +158,6 @@ func simpleMergeMeta(meta map[string]interface{}, metaUpdate map[string]interfac
func setMetaRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("setmeta", rtnErr == nil)
}()
var jsonMeta map[string]interface{}
if setMetaJsonFilePath != "" {
diff --git a/cmd/wsh/cmd/wshcmd-setvar.go b/cmd/wsh/cmd/wshcmd-setvar.go
index bbfb3e15a1..ea0918fee5 100644
--- a/cmd/wsh/cmd/wshcmd-setvar.go
+++ b/cmd/wsh/cmd/wshcmd-setvar.go
@@ -58,7 +58,6 @@ func parseKeyValue(arg string) (key, value string, err error) {
func setVarRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("setvar", rtnErr == nil)
}()
// Resolve block to get zoneId
diff --git a/cmd/wsh/cmd/wshcmd-ssh.go b/cmd/wsh/cmd/wshcmd-ssh.go
index 4eb1d42a4e..cdcdff319a 100644
--- a/cmd/wsh/cmd/wshcmd-ssh.go
+++ b/cmd/wsh/cmd/wshcmd-ssh.go
@@ -39,7 +39,6 @@ func init() {
func sshRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("ssh", rtnErr == nil)
}()
sshArg := args[0]
diff --git a/cmd/wsh/cmd/wshcmd-tabindicator.go b/cmd/wsh/cmd/wshcmd-tabindicator.go
index c3fa499cf9..56d892b1c3 100644
--- a/cmd/wsh/cmd/wshcmd-tabindicator.go
+++ b/cmd/wsh/cmd/wshcmd-tabindicator.go
@@ -43,7 +43,6 @@ func init() {
func tabIndicatorRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("tabindicator", rtnErr == nil)
}()
fmt.Fprintf(os.Stderr, "tabindicator is deprecated, use 'wsh badge' instead\n")
diff --git a/cmd/wsh/cmd/wshcmd-term.go b/cmd/wsh/cmd/wshcmd-term.go
index f2119ad5b7..6a5f83ab2a 100644
--- a/cmd/wsh/cmd/wshcmd-term.go
+++ b/cmd/wsh/cmd/wshcmd-term.go
@@ -32,7 +32,6 @@ func init() {
func termRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("term", rtnErr == nil)
}()
var cwd string
diff --git a/cmd/wsh/cmd/wshcmd-termscrollback.go b/cmd/wsh/cmd/wshcmd-termscrollback.go
index 6368e1559d..995a07eb0a 100644
--- a/cmd/wsh/cmd/wshcmd-termscrollback.go
+++ b/cmd/wsh/cmd/wshcmd-termscrollback.go
@@ -45,7 +45,6 @@ func init() {
func termScrollbackRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("termscrollback", rtnErr == nil)
}()
// Resolve the block argument
diff --git a/cmd/wsh/cmd/wshcmd-view.go b/cmd/wsh/cmd/wshcmd-view.go
index 1ba84b516f..2347cea407 100644
--- a/cmd/wsh/cmd/wshcmd-view.go
+++ b/cmd/wsh/cmd/wshcmd-view.go
@@ -43,7 +43,6 @@ func init() {
func viewRun(cmd *cobra.Command, args []string) (rtnErr error) {
cmdName := cmd.Name()
defer func() {
- sendActivity(cmdName, rtnErr == nil)
}()
if len(args) == 0 {
OutputHelpMessage(cmd)
diff --git a/cmd/wsh/cmd/wshcmd-wavepath.go b/cmd/wsh/cmd/wshcmd-wavepath.go
index 9a5ad6af39..bdb54d4367 100644
--- a/cmd/wsh/cmd/wshcmd-wavepath.go
+++ b/cmd/wsh/cmd/wshcmd-wavepath.go
@@ -30,7 +30,6 @@ func init() {
func wavepathRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("wavepath", rtnErr == nil)
}()
if len(args) == 0 {
diff --git a/cmd/wsh/cmd/wshcmd-web.go b/cmd/wsh/cmd/wshcmd-web.go
index bfda76b82c..3ffdc93d13 100644
--- a/cmd/wsh/cmd/wshcmd-web.go
+++ b/cmd/wsh/cmd/wshcmd-web.go
@@ -97,7 +97,6 @@ func webGetRun(cmd *cobra.Command, args []string) error {
func webOpenRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("web", rtnErr == nil)
}()
var replaceBlockORef *waveobj.ORef
diff --git a/cmd/wsh/cmd/wshcmd-wsl.go b/cmd/wsh/cmd/wshcmd-wsl.go
index cfe9cd47d8..ee67f7a945 100644
--- a/cmd/wsh/cmd/wshcmd-wsl.go
+++ b/cmd/wsh/cmd/wshcmd-wsl.go
@@ -30,7 +30,6 @@ func init() {
func wslRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
- sendActivity("wsl", rtnErr == nil)
}()
var err error
diff --git a/db/migrations-wstore/000012_drop_telemetry.down.sql b/db/migrations-wstore/000012_drop_telemetry.down.sql
new file mode 100644
index 0000000000..bcdcbcff58
--- /dev/null
+++ b/db/migrations-wstore/000012_drop_telemetry.down.sql
@@ -0,0 +1 @@
+-- Recreating these tables is not necessary; the data is obsolete
diff --git a/db/migrations-wstore/000012_drop_telemetry.up.sql b/db/migrations-wstore/000012_drop_telemetry.up.sql
new file mode 100644
index 0000000000..2650b693e0
--- /dev/null
+++ b/db/migrations-wstore/000012_drop_telemetry.up.sql
@@ -0,0 +1,2 @@
+DROP TABLE IF EXISTS db_tevent;
+DROP TABLE IF EXISTS db_activity;
diff --git a/docs/docs/ai-presets.mdx b/docs/docs/ai-presets.mdx
deleted file mode 100644
index 6321dae3ad..0000000000
--- a/docs/docs/ai-presets.mdx
+++ /dev/null
@@ -1,253 +0,0 @@
----
-sidebar_position: 3.6
-id: "ai-presets"
-title: "AI Presets (Deprecated)"
----
-:::warning Deprecation Notice
-The AI Widget and its presets are being replaced by [Wave AI](./waveai.mdx). Please refer to the Wave AI documentation for the latest AI features and configuration options.
-:::
-
-
-
-
-Wave's AI widget can be configured to work with various AI providers and models through presets. Presets allow you to define multiple AI configurations and easily switch between them using the dropdown menu in the AI widget.
-
-## How AI Presets Work
-
-AI presets are defined in `~/.config/waveterm/presets/ai.json`. You can easily edit this file using:
-
-```bash
-wsh editconfig presets/ai.json
-```
-
-Each preset defines a complete set of configuration values for the AI widget. When you select a preset from the dropdown menu, those configuration values are applied to the widget. If no preset is selected, the widget uses the default values from `settings.json`.
-
-Here's a basic example using Claude:
-
-```json
-{
- "ai@claude-sonnet": {
- "display:name": "Claude 3 Sonnet",
- "display:order": 1,
- "ai:*": true,
- "ai:apitype": "anthropic",
- "ai:model": "claude-3-5-sonnet-latest",
- "ai:apitoken": ""
- }
-}
-```
-
-To make a preset your default, add this single line to your `settings.json`:
-
-```json
-{
- "ai:preset": "ai@claude-sonnet"
-}
-```
-
-:::info
-You can quickly set your default preset using the `setconfig` command:
-
-```bash
-wsh setconfig ai:preset=ai@claude-sonnet
-```
-
-This is easier than editing settings.json directly!
-:::
-
-## Provider-Specific Configurations
-
-### Anthropic (Claude)
-
-To use Claude models, create a preset like this:
-
-```json
-{
- "ai@claude-sonnet": {
- "display:name": "Claude 3 Sonnet",
- "display:order": 1,
- "ai:*": true,
- "ai:apitype": "anthropic",
- "ai:model": "claude-3-5-sonnet-latest",
- "ai:apitoken": ""
- }
-}
-```
-
-### OpenAI
-
-To use OpenAI's models:
-
-```json
-{
- "ai@openai-gpt41": {
- "display:name": "GPT-4.1",
- "display:order": 2,
- "ai:*": true,
- "ai:model": "gpt-4.1",
- "ai:apitoken": ""
- }
-}
-```
-
-### Local LLMs (Ollama)
-
-To connect to a local Ollama instance:
-
-```json
-{
- "ai@ollama-llama": {
- "display:name": "Ollama - Llama2",
- "display:order": 3,
- "ai:*": true,
- "ai:baseurl": "http://localhost:11434/v1",
- "ai:name": "llama2",
- "ai:model": "llama2",
- "ai:apitoken": "ollama"
- }
-}
-```
-
-Note: The `ai:apitoken` is required but can be any value as Ollama ignores it. See [Ollama OpenAI compatibility docs](https://github.com/ollama/ollama/blob/main/docs/openai.md) for more details.
-
-### Azure OpenAI
-
-To connect to Azure AI services:
-
-```json
-{
- "ai@azure-gpt4": {
- "display:name": "Azure GPT-4",
- "display:order": 4,
- "ai:*": true,
- "ai:apitype": "azure",
- "ai:baseurl": "",
- "ai:model": "",
- "ai:apitoken": ""
- }
-}
-```
-
-Note: Do not include query parameters or `api-version` in the `ai:baseurl`. The `ai:model` should be your model deployment name in Azure.
-
-### Perplexity
-
-To use Perplexity's models:
-
-```json
-{
- "ai@perplexity-sonar": {
- "display:name": "Perplexity Sonar",
- "display:order": 5,
- "ai:*": true,
- "ai:apitype": "perplexity",
- "ai:model": "llama-3.1-sonar-small-128k-online",
- "ai:apitoken": ""
- }
-}
-```
-
-### Google (Gemini)
-
-To use Google's Gemini models from [Google AI Studio](https://aistudio.google.com):
-
-```json
-{
- "ai@gemini-2.0": {
- "display:name": "Gemini 2.0",
- "display:order": 6,
- "ai:*": true,
- "ai:apitype": "google",
- "ai:model": "gemini-2.0-flash-exp",
- "ai:apitoken": ""
- }
-}
-```
-
-### OpenRouter
-
-To use OpenRouter's models:
-
-```json
-{
- "ai@openrouter": {
- "display:name": "OpenRouter (Qwen)",
- "display:order": 7,
- "ai:*": true,
- "ai:model": "qwen/qwen3-next-80b-a3b-thinking",
- "ai:apitoken": "",
- "ai:baseurl": "https://openrouter.ai/api/v1"
- }
-}
-```
-
-## Multiple Presets Example
-
-You can define multiple presets in your `ai.json` file:
-
-```json
-{
- "ai@claude-sonnet": {
- "display:name": "Claude 3 Sonnet",
- "display:order": 1,
- "ai:*": true,
- "ai:apitype": "anthropic",
- "ai:model": "claude-3-5-sonnet-latest",
- "ai:apitoken": ""
- },
- "ai@openai-gpt41": {
- "display:name": "GPT-4.1",
- "display:order": 2,
- "ai:*": true,
- "ai:model": "gpt-4.1",
- "ai:apitoken": ""
- },
- "ai@ollama-llama": {
- "display:name": "Ollama - Llama2",
- "display:order": 3,
- "ai:*": true,
- "ai:baseurl": "http://localhost:11434/v1",
- "ai:name": "llama2",
- "ai:model": "llama2",
- "ai:apitoken": "ollama"
- },
- "ai@perplexity-sonar": {
- "display:name": "Perplexity Sonar",
- "display:order": 4,
- "ai:*": true,
- "ai:apitype": "perplexity",
- "ai:model": "llama-3.1-sonar-small-128k-online",
- "ai:apitoken": ""
- }
-}
-```
-
-The `display:order` value determines the order in which presets appear in the dropdown menu.
-
-Remember to set your default preset in `settings.json`:
-
-```json
-{
- "ai:preset": "ai@claude-sonnet"
-}
-```
-
-## Using a Proxy
-
-If you need to route AI requests through an HTTP proxy, you can add the `ai:proxyurl` setting to any preset:
-
-```json
-{
- "ai@claude-with-proxy": {
- "display:name": "Claude 3 Sonnet (via Proxy)",
- "display:order": 1,
- "ai:*": true,
- "ai:apitype": "anthropic",
- "ai:model": "claude-3-5-sonnet-latest",
- "ai:apitoken": "",
- "ai:proxyurl": "http://proxy.example.com:8080"
- }
-}
-```
-
-The proxy URL should be in the format `http://host:port` or `https://host:port`. This setting works with all AI providers except Wave Cloud AI (the default).
diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx
index 05389e99ef..63020fb229 100644
--- a/docs/docs/config.mdx
+++ b/docs/docs/config.mdx
@@ -40,22 +40,10 @@ wsh editconfig
| app:showoverlayblocknums | bool | Set to false to disable the Ctrl+Shift block number overlay that appears when holding Ctrl+Shift (defaults to true) |
| app:ctrlvpaste | bool | On Windows/Linux, when null (default) uses Control+V on Windows only. Set to true to force Control+V on all non-macOS platforms, false to disable the accelerator. macOS always uses Command+V regardless of this setting |
| app:confirmquit | bool | Set to false to disable the quit confirmation dialog when closing Wave Terminal (defaults to true, requires app restart) |
-| app:hideaibutton | bool | Set to true to hide the AI button in the tab bar (defaults to false) |
| app:disablectrlshiftarrows | bool | Set to true to disable Ctrl+Shift block-navigation keybindings (`Arrow` and `h/j/k/l`) (defaults to false) |
| app:disablectrlshiftdisplay | bool | Set to true to disable the Ctrl+Shift visual indicator display (defaults to false) |
| app:focusfollowscursor | string | Controls whether block focus follows cursor movement: `"off"` (default), `"on"` (all blocks), or `"term"` (terminal blocks only) |
| app:tabbar | string | Controls the position of the tab bar: `"top"` (default) for a horizontal tab bar at the top of the window, or `"left"` for a vertical tab bar on the left side of the window |
-| ai:preset | string | the default AI preset to use |
-| ai:baseurl | string | Set the AI Base Url (must be OpenAI compatible) |
-| ai:apitoken | string | your AI api token |
-| ai:apitype | string | defaults to "open_ai", but can also set to "azure" (forspecial Azure AI handling), "anthropic", or "perplexity" |
-| ai:name | string | string to display in the Wave AI block header |
-| ai:model | string | model name to pass to API |
-| ai:apiversion | string | for Azure AI only (when apitype is "azure", this will default to "2023-05-15") |
-| ai:orgid | string | |
-| ai:maxtokens | int | max tokens to pass to API |
-| ai:timeoutms | int | timeout (in milliseconds) for AI calls |
-| ai:proxyurl | string | HTTP proxy URL for AI API requests (does not apply to Wave Cloud AI) |
| conn:askbeforewshinstall | bool | set to false to disable popup asking if you want to install wsh extensions on new machines |
| conn:localhostdisplayname | string | override the display name for localhost in the UI (e.g., set to "My Laptop" or "Local", or set to empty string to hide the name) |
| term:fontsize | float | the fontsize for the terminal block |
@@ -115,19 +103,13 @@ wsh editconfig
| window:savelastwindow | bool | when `true`, the last window that is closed is preserved and is reopened the next time the app is launched (defaults to `true`) |
| window:confirmonclose | bool | when `true`, a prompt will ask a user to confirm that they want to close a window if it has an unsaved workspace with more than one tab (defaults to `true`) |
| window:dimensions | string | set the default dimensions for new windows using the format "WIDTHxHEIGHT" (e.g. "1920x1080"). when a new window is created, these dimensions will be automatically applied. The width and height values should be specified in pixels. |
-| telemetry:enabled | bool | set to enable/disable telemetry |
For reference, this is the current default configuration (v0.14.0):
```json
{
- "ai:preset": "ai@global",
- "ai:model": "gpt-5-mini",
- "ai:maxtokens": 4000,
- "ai:timeoutms": 60000,
"app:defaultnewblock": "term",
"app:confirmquit": true,
- "app:hideaibutton": false,
"app:disablectrlshiftarrows": false,
"app:disablectrlshiftdisplay": false,
"app:focusfollowscursor": "off",
@@ -148,17 +130,13 @@ For reference, this is the current default configuration (v0.14.0):
"window:fullscreenonlaunch": false,
"window:magnifiedblockblursecondarypx": 2,
"window:confirmclose": true,
- "window:savelastwindow": true,
- "telemetry:enabled": true,
- "term:bellsound": false,
+ "window:savelastwindow": true, "term:bellsound": false,
"term:bellindicator": false,
"term:osc52": "always",
"term:cursor": "block",
"term:cursorblink": false,
"term:copyonselect": true,
"term:durable": false,
- "waveai:showcloudmodes": true,
- "waveai:defaultmode": "waveai@balanced",
"preview:defaultsort": "name"
}
```
@@ -173,12 +151,12 @@ files as well: `termthemes.json`, `presets.json`, and `widgets.json`.
## Environment Variable Resolution
-To avoid putting secrets directly in config files, Wave supports environment variable resolution using `$ENV:VARIABLE_NAME` or `$ENV:VARIABLE_NAME:fallback` syntax. This works for any string value in any config file (settings.json, presets.json, ai.json, etc.).
+To avoid putting secrets directly in config files, Wave supports environment variable resolution using `$ENV:VARIABLE_NAME` or `$ENV:VARIABLE_NAME:fallback` syntax. This works for any string value in any config file (settings.json, presets.json, etc.).
```json
{
- "ai:apitoken": "$ENV:OPENAI_APIKEY",
- "ai:baseurl": "$ENV:AI_BASEURL:https://api.openai.com/v1"
+ "conn:sshpassword": "$ENV:SSH_PASSWORD",
+ "web:defaulturl": "$ENV:HOME_PAGE:https://example.com"
}
```
diff --git a/docs/docs/faq.mdx b/docs/docs/faq.mdx
index 61dc80beb4..69dc81cb7f 100644
--- a/docs/docs/faq.mdx
+++ b/docs/docs/faq.mdx
@@ -56,15 +56,3 @@ If you've installed via Snap, you can use the following command:
```sh
sudo snap install waveterm --classic --beta
```
-
-## Can I use Wave AI without enabling telemetry?
-
-
-
-Yes! Wave AI is normally disabled when telemetry is not enabled. However, you can enable Wave AI features without telemetry by configuring your own custom AI model (either a local model or using your own API key).
-
-To enable Wave AI without telemetry:
-1. Configure a custom AI mode (see [Wave AI documentation](./waveai-modes))
-2. Set `waveai:defaultmode` to your custom mode's key in your Wave settings
-
-Once you've completed both steps, Wave AI will be enabled and you can use it completely privately without telemetry. This allows you to use local models like Ollama or your own API keys with providers like OpenAI, OpenRouter, or others.
diff --git a/docs/docs/gettingstarted.mdx b/docs/docs/gettingstarted.mdx
index 7ff961a9a9..5775055ef7 100644
--- a/docs/docs/gettingstarted.mdx
+++ b/docs/docs/gettingstarted.mdx
@@ -7,7 +7,7 @@ title: "Getting Started"
import { PlatformProvider, PlatformSelectorButton, PlatformItem } from "@site/src/components/platformcontext";
import { Kbd } from "@site/src/components/kbd";
-Wave Terminal is a modern terminal that includes graphical capabilities like web browsing, file previews, and AI assistance alongside traditional terminal features. This guide will help you get started.
+Wave Terminal is a modern terminal that includes graphical capabilities like web browsing and file previews alongside traditional terminal features. This guide will help you get started.
## Installation
@@ -106,7 +106,6 @@ You can also download installers directly from our [Downloads page](https://www.
- Preview files (images, video, markdown, code with syntax highlighting)
- Browse web pages
- - Ask questions and get AI help directly from the terminal (set up multiple AI models)
- Basic system monitoring graphs
3. **Remote Connections**
@@ -131,8 +130,6 @@ You can also download installers directly from our [Downloads page](https://www.
# Open a webpage
wsh web open github.com
- # Get AI assistance
- wsh ai -m "how do I find large files in my current directory?" -s
```
3. **Customize Your Layout**
@@ -152,7 +149,6 @@ You can also download installers directly from our [Downloads page](https://www.
- Explore [Key Bindings](./keybindings) to work more efficiently
- Learn about [Tab Layouts](./layout) to organize your workspace
- Set up [Custom Widgets](./customwidgets) for quick access to your tools
-- Configure [Wave AI](./waveai) to use your preferred AI models
- Check out [Configuration](./config) for detailed customization options
## Getting Help
diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx
index f1665faae8..b92b270a5e 100644
--- a/docs/docs/index.mdx
+++ b/docs/docs/index.mdx
@@ -10,21 +10,15 @@ import { Card, CardGroup } from "@site/src/components/card";
# Welcome to Wave Terminal
-Wave is an [open-source](https://github.com/wavetermdev/waveterm) terminal that combines traditional terminal features with graphical capabilities like file previews, web browsing, and AI assistance. It runs on MacOS, Linux, and Windows.
+Wave is an [open-source](https://github.com/wavetermdev/waveterm) terminal that combines traditional terminal features with graphical capabilities like file previews and web browsing. It runs on MacOS, Linux, and Windows.
-Modern development involves constantly switching between terminals and browsers - checking documentation, previewing files, monitoring systems, and using AI tools. Wave brings these graphical tools directly into the terminal, letting you control them from the command line. This means you can stay in your terminal workflow while still having access to the visual interfaces you need.
+Modern development involves constantly switching between terminals and browsers - checking documentation, previewing files, and monitoring systems. Wave brings these graphical tools directly into the terminal, letting you control them from the command line. This means you can stay in your terminal workflow while still having access to the visual interfaces you need.
Check out [Getting Started](./gettingstarted) for installation instructions.

- | Open a new tab |
| | Open a new block (defaults to a terminal block with the same connection and working directory). Switch to launcher using `app:defaultnewblock` setting |
-| | Toggle WaveAI panel visibility |
| | Split horizontally, open a new block to the right |
| | Split vertically, open a new block below |
| | Split vertically, open a new block above |
@@ -41,7 +40,6 @@ Chords are shown with a + between the keys. You have 2 seconds to hit the 2nd ch
| | Open the "connection" switcher |
| | Refocus the current block (useful if the block has lost input focus) |
| | Show block numbers |
-| | Focus WaveAI input |
| | Switch to block number |
| / | Move left, right, up, down between blocks |
| | Replace the current block with a launcher block |
@@ -81,14 +79,6 @@ Chords are shown with a + between the keys. You have 2 seconds to hit the 2nd ch
| | Find in webpage |
| | Open a bookmark |
-## WaveAI Keybindings
-
-| Key | Function |
-| ----------------------- | ----------------------- |
-| | Toggle WaveAI panel |
-| | Focus WaveAI input |
-| | Clear AI Chat |
-
## Terminal Keybindings
| Key | Function |
diff --git a/docs/docs/releasenotes.mdx b/docs/docs/releasenotes.mdx
index 987be81534..c0c5120bb5 100644
--- a/docs/docs/releasenotes.mdx
+++ b/docs/docs/releasenotes.mdx
@@ -40,8 +40,7 @@ Wave v0.14.4 introduces vertical tabs, upgrades to xterm.js v6, and includes a c
- **Config Errors Moved** - Config errors removed from the tab bar and moved to Settings / WaveConfig view for less clutter
- **Warn on Unsaved Changes** - WaveConfig view now warns before discarding unsaved changes
- **Stream Performance** - Migrated file streaming to new modern interface with flow control, fixing a large time-to-first-byte streaming bug
-- **macOS First Click** - Improved first-click handling on macOS (cancel the click but properly set block/WaveAI focus)
-- Deprecated legacy AI widget has been removed
+- **macOS First Click** - Improved first-click handling on macOS (cancel the click but properly set block focus)
- [bugfix] Fixed focus bug for newly created blocks
- [bugfix] Fixed an issue around starting a new durable session by splitting an old one
- Electron upgraded to v41
@@ -65,8 +64,6 @@ Wave v0.14.2 adds block/tab badges, directory preview improvements, and assorted
- [bugfix] Fixed "Save Session As..." (focused window tracking bug)
- [bugfix] Zoom change notifications were not being properly sent to all tabs (layout inconsistencies)
- Added a Release Notes link in the settings menu
-- Working on anthropic-messages Wave AI backend (for native Claude integration)
-- Lots of internal work on testing/mock infrastructure to enable quicker async AI edits
- Documention updates
- Package updates and dependency upgrades
@@ -89,7 +86,6 @@ Wave v0.14.1 fixes several high-impact terminal bugs (Claude Code scrolling, IME
- **Tab Close Confirmation** - New `tab:confirmclose` setting to prompt before closing a tab
- **Workspace-Scoped Widgets** - New optional `workspaces` field in `widgets.json` to show/hide widgets per-workspace
- **Vim-Style Block Navigation** - Added Ctrl+Shift+H/J/K/L to navigate between blocks
-- **New AI Providers** - Added Groq and NanoGPT as built-in AI provider presets
**Other Changes:**
@@ -121,20 +117,12 @@ Wave v0.14 introduces Durable Sessions for SSH connections, allowing your remote
- **Enhanced Context Menu** - Right-click terminals for quick access to splits, URL opening, themes, file browser, and more
- **Streamlined Header Layout** - Terminal headers now focus on connection info without redundant view type labels
-**Wave AI Updates:**
-- **Image/Vision Support** - Added image support for OpenAI chat completions API, enabling vision capabilities with compatible models
-- **Stop Generation** - New ability to stop AI responses mid-generation across OpenAI and Gemini backends
-- **AI Panel Scroll Latch** - Improved auto-scrolling behavior in Wave AI panel
-- **Configurable Verbosity** - Control verbosity levels for OpenAI Responses API
-- Deprecated old AI-widget proxy endpoint
-
**RPC and Performance:**
- **RPC Streaming with Flow Control** - New streaming primitives with built-in flow control for better performance and reliability
- **WSH Router Refactor** - Major routing architecture improvements to prevent hangs on connection interruptions
- **RPC Client/Server Cleanup** - Improved RPC implementation and error handling
**Configuration Updates:**
-- **Hide AI Button** - New `app:hideaibutton` setting to hide the AI button from the UI
- **Disable Ctrl+Shift Arrows** - New `app:disablectrlshiftarrows` setting for keyboard shortcut conflicts
- **Disable Ctrl+Shift Display** - New `app:disablectrlshiftdisplay` setting to disable overlay block numbers
@@ -149,550 +137,11 @@ Wave v0.14 introduces Durable Sessions for SSH connections, allowing your remote
- Removed OSC 23198 and OSC 9283 legacy handlers
- Updated contribution guidelines
- Upgraded Go toolchain to 1.25.6
-- Enhanced OpenAI-compatible API provider documentation
- [bugfix] Fixed empty data handling in sysinfo view
- [bugfix] Fixed `app:ctrlvpaste` setting on Windows (can now be disabled)
-- [bugfix] Fixed duplicated Wave AI system prompt for some providers
- [bugfix] Fixed disconnect hanging issue - disconnects now happen immediately
- [bugfix] Fixed tool approval lifecycle to match SSE connection timing
- [bugfix] Increased WSL connection timeout to handle slow initial WSL startup
- [bugfix] Improved terminal shutdown with SIGHUP for graceful shell exit
- Package updates and dependency upgrades
-### v0.13.1 — Dec 16, 2025
-
-**Windows Improvements and Wave AI Enhancements**
-
-This release focuses on significant Windows platform improvements, Wave AI visual updates, and better flexibility for local AI usage.
-
-**Windows Platform Enhancements:**
-- **Integrated Window Layout** - Removed separate title bar and menu bar on Windows, integrating controls directly into the tab-bar header for a cleaner, more unified interface
-- **Git Bash Auto-Detection** - Wave now automatically detects Git Bash installations and adds them to the connection dropdown for easy access
-- **SSH Agent Fallback** - Improved SSH agent support with automatic fallback to `\\.\pipe\openssh-ssh-agent` on Windows
-- **Updated Focus Keybinding** - Wave AI focus key changed to Alt:0 on Windows for better consistency
-- **Config Schemas** - Improved configuration validation and schema support
-- Ctrl-V now works as standard paste in terminal on Windows
-
-**Wave AI Updates:**
-- **Refreshed Visual Design** - Complete UI refresh removing blue accents and adding transparency support for better integration with custom backgrounds
-- **BYOK Without Telemetry** - Wave AI now works with bring-your-own-key and local models without requiring telemetry to be enabled
-- [bugfix] Fixed tool type "function" compatibility with providers like Mistral
-
-**Terminal Improvements:**
-- **New Scrolling Keybindings** - Added Shift+Home, Shift+End, Shift+PageUp, and Shift+PageDown for better terminal navigation
-
-**Other Changes:**
-- Package updates and dependency upgrades
-
-### v0.13.0 — Dec 8, 2025
-
-**Wave v0.13 Brings Local AI Support, BYOK, and Unified Configuration**
-
-Wave v0.13 is a major release that opens up Wave AI to local models, third-party providers, and bring-your-own-key (BYOK) configurations. This release also includes a completely redesigned configuration system and several terminal improvements.
-
-**Local AI & BYOK Support:**
-- **OpenAI-Compatible API** - Wave now supports any provider or local server using the `/v1/chat/completions` endpoint, enabling use of Ollama, LM Studio, vLLM, OpenRouter, and countless other local and hosted models
-- **Google Gemini Integration** - Native support for Google's Gemini models with a dedicated API adapter
-- **Provider Presets** - Simplified configuration with built-in presets for OpenAI, OpenRouter, Google, Azure, and custom endpoints
-- **Multiple AI Modes** - Easily switch between different models and providers with a unified interface
-- See the new [Wave AI Modes documentation](https://docs.waveterm.dev/waveai-modes) for configuration examples and setup guides
-
-**Unified Configuration Widget:**
-- **New Config Interface** - Replaced the basic JSON editor with a dedicated configuration widget accessible from the sidebar
-- **Better Organization** - Browse and edit different configuration types (general settings, AI modes, secrets) with improved validation and error handling
-- **Integrated Secrets Management** - Access Wave's secret store directly from the config widget for secure credential management
-
-**Terminal Improvements:**
-- **Bracketed Paste Mode** - Now enabled by default to improve multi-line paste behavior and compatibility with tools like Claude Code
-- **Windows Paste Fix** - Ctrl+V now works as a standard paste accelerator on Windows
-- **SSH Password Management** - Store SSH connection passwords in Wave's secret store to avoid re-typing credentials
-
-**Other Changes:**
-- Package updates and dependency upgrades
-- Various bug fixes and stability improvements
-
-### v0.12.5 — Nov 24, 2025
-
-Quick patch release to fix paste behavior on Linux (prevent raw HTML from getting pasted to the terminal).
-
-### v0.12.4 — Nov 21, 2025
-
-Quick patch release with bug fixes and minor improvements.
-
-- New `term:macoptionismeta` setting for macOS to treat Option key as Meta key in terminal
-- Fixed directory tracking for zsh shells
-- Fixed editor copy operations
-- Minor Wave AI improvements (image handling, scrolling, focus)
-- Package updates and dependency upgrades
-- WIP: WaveApps builder framework (not yet released)
-
-### v0.12.3 — Nov 17, 2025
-
-Patch release with Wave AI model upgrade, new secret management features, and improved terminal input handling.
-
-**Wave AI Updates:**
-- **GPT-5.1 Model** - Upgraded to use OpenAI's GPT-5.1 model for improved responses
-- **Thinking Mode Toggle** - New dropdown to select between Quick, Balanced, and Deep thinking modes for optimal response quality vs speed
-- [bugfix] Fixed path mismatch issue when restoring AI write file backups
-
-**New Features:**
-- **Secret Store** - New secret management widget for storing and managing sensitive credentials. Access secrets via CLI with `wsh secret list/get/set` commands
-
-**Terminal Improvements:**
-- **Enhanced Input Handling** - Better support for interactive CLI tools like Claude Code. Shift+Enter now inserts newlines by default for multi-line commands
-- **Image Paste Support** - Paste images directly into terminal (saved to temp files with path inserted). Works great in Claude Code!
-- **IME Fix** - Fixed duplicate text issue when switching input methods during Chinese/Japanese/Korean composition
-
-**Other Changes:**
-- Improved backend panic tracking for better debugging
-- Fixed memory leak around sysinfo events
-- WIP: New WaveApps builder framework (not yet released)
-- Package updates and dependency bumps
-
-### v0.12.2 — Nov 4, 2025
-
-Wave v0.12.2 adds file editing ability to Wave AI. Before approving a file edit you can easily see a diff (rendered in the Monaco Editor diff viewer), and after approving an edit you can easily roll back the change using a "Revert File" button.
-
-**Wave AI Updates:**
-- **File Write Tool** - Wave AI can now create and modify files with your approval
-- **Visual Diff Preview** - See exactly what will change before approving edits, rendered in Monaco Editor
-- **Easy Rollback** - Revert file changes with a simple "Revert File" button
-- **Drag & Drop Files** - Drag files from the preview viewer directly to Wave AI
-- **Directory Listings** - `wsh ai` can now attach directory listings to chats
-- **Adjustable Settings** - Control thinking level and max output tokens per chat
-
-**Bug Fixes & Improvements:**
-- Fixed a significant memory leak in the RPC system
-- Schema validation working again for config files
-- Improved tool descriptions and input validations (run before tool approvals)
-- Fixed issue with premature tool timeouts
-- Fixed regression with PowerShell 5.x
-- Fixed prompt caching issue when attaching files
-
-### v0.12.1 — Oct 20, 2025
-
-Patch release focused on shell integration improvements and Wave AI enhancements. This release fixes syntax highlighting in the code editor and adds significant shell context tracking capabilities.
-
-**Shell Integration & Context:**
-- **OSC 7 Support** - Added OSC 7 (current working directory) support across bash, zsh, fish, and pwsh shells. Wave now automatically tracks and restores your current directory across restarts for both local and remote terminals.
-- **Shell Context Tracking** - Implemented shell integration for bash, zsh, and fish shells. Wave now tracks when your shell is ready to receive commands, the last command executed, and exit codes. This enhanced context enables better terminal management and lays the groundwork for Wave AI to write and execute commands intelligently.
-
-**Wave AI Improvements:**
-- Display reasoning summaries in the UI while waiting for AI responses
-- Added enhanced terminal context - Wave AI now has access to shell state including current directory, command history, and exit codes
-- Added feedback buttons (thumbs up/down) for AI responses to help improve the experience
-- Added copy button to easily copy AI responses to clipboard
-
-**Other Changes:**
-- Mobile user agent emulation support for web widgets [#2442](https://github.com/wavetermdev/waveterm/issues/2442)
-- [bugfix] Fixed padding for header buttons in code editor (Tailwind regression)
-- [bugfix] Restored syntax highlighting in code editor preview blocks [#2427](https://github.com/wavetermdev/waveterm/issues/2427)
-- Package updates and dependency bumps
-
-### v0.12.0 — Oct 16, 2025
-
-**Wave v0.12 Has Arrived with Wave AI (beta)!**
-
-Wave Terminal v0.12.0 introduces a completely redesigned AI experience powered by OpenAI GPT-5. This represents a major upgrade and modernization over Wave's previous AI integration, bringing multi-modal support, advanced tool integration, and an intuitive new interface. The main AI PR alone included 128 commits and added 13,000+ lines of code.
-
-**Wave AI Features:**
-- **New Slide-Out Chat Panel** - Access Wave AI via hotkeys (Cmd-Shift-A or Ctrl-Shift-0) from the left side of your screen
-- **Multi-Modal Input** - Support for images, PDFs, and text file attachments
-- **Drag & Drop Files** - Simply drag files into the chat to attach them
-- **Command-Line Integration** - Send files and command output directly to Wave AI using `wsh ai`
-- **Smart Context Switching** - Enable Wave AI to see into your widgets and file system
-- **Built-in Tools:**
- - Web search capabilities
- - Local file and directory operations
- - Widget screenshots
- - Terminal scrollback access
- - Web navigation
-
-Wave AI is in active beta with included AI credits while we refine the experience. BYOK (Bring Your Own Key) will be available once we've stabilized core features and gathered feedback on what works best. Share your feedback in our [Discord](https://discord.gg/XfvZ334gwU).
-
-For more information and upcoming features, visit our [Wave AI documentation](https://docs.waveterm.dev/waveai).
-
-**Other Improvements:**
-- New onboarding flow showcasing block magnification, Wave AI, and wsh view/edit capabilities
-- New `wsh blocks list` command for listing and filtering blocks by workspace, tab, or view type
-- Continued migration from SCSS to Tailwind v4
-- Package upgrades and dependency updates
-- Internal code cleanup and refactoring
-
-### v0.11.6 — Sep 22, 2025
-
-Patch release to address an editor bug when you open two files in separate edit widgets. Also adds Mermaid support to markdown blocks.
-
-* WIP: Big AI overhaul coming (multi-modal support, premium models, and tool support)
-* WIP: Integrating new Tsunami widget framework to make writing and running Wave widgets easier
-* Lots of package updates
-* Much internal cleanup (preview widget)
-* More migration to Tailwind v4 CSS
-* Build updates, switched to npm from yarn
-
-### v0.11.5 — Aug 28, 2025
-
-Another housekeeping release to modernize Wave and bring it more up to date.
-
-* Wave AI Cloud Proxy now uses gpt-5-mini (upgraded from gpt-4o-mini)
-* Fixed JWT issue with running "Wave Apps" from widgets
-* Added an "$ENV:envvar:fallback" syntax to the config files to allow Wave's config to pick up values from the environment (mostly to allow moving secrets out of the config files)
-* New setting to disable showing overlay blocknums when holding Ctrl:Shift (`app:showoverlayblocknums`)
-* New setting to allow Shift-Enter to work with tools like Claude Code (`term:shiftenternewline`)
-* Upgraded frontend to React 19
-* Migrated more of the frontend to Tailwind v4 (work in progress)
-* Removed Universal MacOS build. 90% of Mac users are now on Apple Silicon, so universal build is less important (has a larger file size, and complicates the build process).
-* [bugfix] Removed build-ids in RPM build to try to fix conflicts with Slack
-* Removed some Wave v7 aware upgrades and old code paths
-* Internal cleanup, TypeScript errors, linting fixes, etc.
-* Other assorted Go/npm package bumps
-
-### v0.11.4 — Aug 19, 2025
-
-Quick patch release to update packages, fix some security issues (with dependent packages), and some small bug fixes.
-
-* Update AI Libraries, GPT-5 now supported in WaveAI
-* Added `ai:proxyurl` setting to allow proxy access (e.g. SOCKS) for AI access
-
-### v0.11.3 — May 2, 2025
-
-Quick patch release to update packages, fix some security issues (with dependent packages), and some small bug fixes.
-
-### v0.11.2 — March 8, 2025
-
-Quick patch release to fix a backend panic, and revert a change that caused WSL connections to hang.
-
-### v0.11.1 — Feb 28, 2025
-
-Wave Terminal v0.11.1 adds a lot of new functionality over v0.11.0 (it could have almost been a v0.12)!
-
-The headline feature is our files/preview widget now supports browsing S3 buckets. We read credential information directly from your ~/.aws/config, and you can now easily select any of your AWS profiles in our connections drop down to start viewing S3 files. We even support editing S3 text files using our built-in editor.
-
-Lots of other features and bug fixes as well:
-
-- **S3 Bucket** directory viewing and file previews
-- **Drag and Drop Files and Directories** between Wave directory views. This works across machines and between remote machines and S3 conections.
-- Added json-schema support for some of our config files. You'll now get auto-complete popups for fields in our settings.json, widgets.json, ai.json, and connections.json file.
-- New block splitting support -- Use Cmd-D and Cmd-Shift-D to split horizontally and vertically. For more control you can use Ctrl-Shift-S and then Up/Down/Left/Right to split in the given direction.
-- Delete block (without removing it from the layout). You can use Ctrl-Shift-D to remove a block, while keeping it in the layout. you can then launch a new widget in its place.
-- `wsh file` now supports copying files between your local machine, remote machines, and to/from S3
-- New analytics framework (event based as opposed to counter based). See Telemetry Docs for more information.
-- Web bookmarks! Edit in your bookmarks.json file, can open them in the web widget using Cmd+O
-- Edits to your ai.json presets file will now take effect _immediately_ in AI widgets
-- Much better error handling and messaging when errors occur in the preview or editor widget
-- `wsh ssh --new` added to open the new ssh connection in a new widget
-- new `wsh launch` command to open any custom widget defined in widget.json
-- When using terminal multi-input (Ctrl-Shift-I), pasting text will now be sent to all terminals
-- [bugfix] Fix some hanging goroutines when commands failed or timed out
-- [bugfix] Fix some file extension mimetypes to enable the editor for more file types
-- [bugfix] Hitting "tab" would sometimes scroll a widget off screen making it unusable
-- [bugfix] XDG variables will no longer leak to terminal widgets
-- Added tailwind CSS and shadcn support to help build new widgets faster
-- Better internal widget abstractions
-
-### v0.11.0 — Jan 24, 2025
-
-Wave Terminal v0.11.0 includes a major rewrite of our connections infrastructure, with changes to both our backend and remote file protocol systems, alongside numerous features, bug fixes, and stability improvements.
-
-A key addition in this release is the new shell initialization system, which enables customization of your shell environment across local and remote connections. You can now configure environment variables and shell-specific init scripts on both a per-block and per-connection basis.
-
-For day-to-day use, we've added search functionality across both terminal and web blocks, along with a terminal multi-input feature for simultaneous input to all terminals within a tab. We've also added support for Google Gemini to Wave AI, expanding our suite of AI integrations.
-
-Behind the scenes, we've redesigned our remote file protocol, laying the groundwork for upcoming S3 (and S3-compatible system) support in our preview widget. This architectural change sets the stage for adding more file backends in the future.
-
-- **Shell Environment Customization** -- Configure your shell environment using environment variables and init scripts, with support for both local and remote connections
-- **Connection Backend Improvements** -- Major rewrite with improved shell detection, better error logging, and reduced 2FA prompts when using ForceCommand
-- **Multi-Shell Support** -- Enhanced support for bash, zsh, pwsh, and fish shells, with shell-specific initialization capabilities
-- **Terminal Search** -- use Cmd-F to search for text in terminal widgets
-- **Web Search** -- use Cmd-F to search for text in web views
-- **Terminal Multi-Input** -- Use Ctrl-Shift-I to allow multi-input to all terminals in the same tab
-- **Wave AI now supports Google Gemini**
-- Improved WSL support with wsh-free connection options
-- Added inline connection debugging information
-- Fixed file permission handling issues on Windows systems
-- Connection related popups are now delivered only to the initiating window
-- Improved timeout handling for SSH connections which require 2FA prompts
-- Fixed escape key handling in global event handlers (closing modals)
-- Directory preview now fills the entire block width
-- Custom widgets can now be launched in magnified mode
-- Various workspace UX improvements around closing/deleting
-- file:/// urls now work in web widget
-- Increased max size of files allowed in `wsh ai` to 50k
-- Increased maximum allowed term:scrollback to 50k lines
-- Allow connections to entirely be defined in connections.json without relying on ~/.ssh/config
-- Added an option to reveal files in external file viewer for local connection
-- Added a New Window option when right clicking the MacOS dock icon button
-- [build] Switched to free Ubuntu ARM runners for better ARM64 build support
-- [build] Windows builds now use zig, simplifying Windows dev setup
-- [bugfix] Connections dropdown now populated even when ssh config is missing or invalid
-- [bugfix] Disabled bracketed paste mode by default (configuration option to turn it back on)
-- [bugfix] Timeout for `wsh ssh` increased to 60s
-- [bugfix] Fix for sysinfo widget when displaying a huge number of CPU graphs
-- [bugfix] Fixes XDG variables for Snap installs
-- [bugfix] Honor SSH IdentitiesOnly flag (useful when many keys are loaded into ssh-agent)
-- [bugfix] Better shell environment variable setup when running local shells
-- [bugfix] Fix preview for large text files
-- [bugfix] Fix URLs in terminal (now clickable again)
-- [bugfix] Windows URLs now work properly for Wave background images
-- [bugfix] Connections launch without wsh if the unix domain socket can't be opened
-- [bugfix] Connection status list lights up correctly with currently connected connections
-- [bugfix] Use en_US.UTF-8 if the requested LANG is not available in your terminal
-- Other bug fixes, performance improvements, and dependency updates
-
-### v0.10.4 — Dec 20, 2024
-
-Quick update with bug fixes and new configuration options
-
-- Added "window:confirmclose" and "window:savelastwindow" configuration options
-- [bugfix] Fixed broken scroll bar in the AI widget
-- [bugfix] Fixed default path for wsh shell detection (used in remote connections)
-- Dependency updates
-
-### v0.10.3 — Dec 19, 2024
-
-Quick update to v0.10 with new features and bug fixes.
-
-- Global hotkey support [docs](https://docs.waveterm.dev/config#customizable-systemwide-global-hotkey)
-- Added configuration to override the font size for markdown, AI-chat, and preview editor [docs](https://docs.waveterm.dev/config)
-- Added ability to set independent zoom level for the web view (right click block header)
-- New `wsh wavepath` command to open the config directory, data directory, and log file
-- [bugfix] Fixed crash when /etc/sshd_config contained an unsupported Match directive (most common on Fedora)
-- [bugfix] Workspaces are now more consistent across windows, closes associated window when Workspaces are deleted
-- [bugfix] Fixed zsh on WSL
-- [bugfix] Fixed long-standing bug around control sequences sometimes showing up in terminal output when switching tabs
-- Lots of new examples in the docs for shell overrides, presets, widgets, and connections
-- Other bug fixes and UI updates
-
-(note, v0.10.2 and v0.10.3's release notes have been merged together)
-
-### v0.10.1 — Dec 12, 2024
-
-Quick update to fix the workspace app menu actions. Also fixes workspace switching to always open a new window when invoked from a non-workspace window. This reduces the chance of losing a non-workspace window's tabs accidentally.
-
-### v0.10.0 — Dec 11, 2024
-
-Wave Terminal v0.10.0 introduces workspaces, making it easier to manage multiple work environments. We've added powerful new command execution capabilities with `wsh run`, allowing you to launch and control commands in dedicated blocks. This release also brings significant improvements to SSH with a new connections configuration system for managing your remote environments.
-
-- **Workspaces**: Organize your work into separate environments, each with their own tabs, layouts, and settings
-- **Command Blocks**: New `wsh run` command for launching terminal commands in dedicated blocks, with support for magnification, auto-closing, and execution control ([docs](https://docs.waveterm.dev/wsh-reference#run))
-- **Connections**: New configuration system for managing SSH connections, with support for wsh-free operation, per-connection themes, and more ([docs](https://docs.waveterm.dev/connections))
-- Improved tab management with better switching behavior and context menus (many bug fixes)
-- New tab features including pinned tabs and drag-and-drop improvements
-- Create, rename, and delete files/directories directly in directory preview
-- Attempt wsh-free connection as a fallback if wsh installation or execution fails
-- New `-i` flag to add identity files with the `wsh ssh` command
-- Added Perplexity API integration ([docs](https://docs.waveterm.dev/faq#perplexity))
-- `wsh setbg` command for background handling ([docs](https://docs.waveterm.dev/wsh-reference#setbg))
-- Switched from Less to SCSS for styling
-- [bugfix] Fixed tab flickering issues during tab switches
-- [bugfix] Corrected WaveAI text area resize behavior
-- [bugfix] Fixed concurrent block controller start issues
-- [bugfix] Fixed Preview Blocks for uninitialized connections
-- [bugfix] Fixed unresponsive context menus
-- [bugfix] Fixed connection errors in Help block
-- Upgraded Go toolchain to 1.23.4
-- Lots of new documentation, including new pages for [Getting Started](https://docs.waveterm.dev/gettingstarted), [AI Presets](https://docs.waveterm.dev/ai-presets), and [wsh overview](https://docs.waveterm.dev/wsh).
-- Other bug fixes, performance improvements, and dependency updates
-
-### v0.9.3 — Nov 20, 2024
-
-New minor release that introduces Wave's connected computing extensions. We've introduced new `wsh` commands that allow you to store variables and files, and access them across terminal sessions (on both local and remote machines).
-
-- `wsh setvar/getvar` to get and set variables -- [Docs](https://docs.waveterm.dev/wsh-reference#getvarsetvar)
-- `wsh file` operations (cat, write, append, rm, info, cp, and ls) -- [Docs](https://docs.waveterm.dev/wsh-reference#file)
-- Improved golang panic handling to prevent backend crashes
-- Improved SSH config logging and fixes a reused connection bug
-- Updated telemetry to track additional counters
-- New configuration settings (under "window:magnifiedblock") to control magnified block margins and display
-- New block/zone aliases (client, global, block, workspace, temp)
-- `wsh ai` file attachments are now rendered with special handling in the AI block
-- New ephemeral block type for creating modal widgets which will not disturb the underlying layout
-- Editing the AI presets file from the Wave AI block now brings up an ephemeral editor
-- Clicking outside of a magnified bglock will now un-magnify it
-- New button to clear the AI chat (also bound to Cmd-L)
-- New button to reset terminal commands in custom cmd widgets
-- [bugfix] Presets directory was not loading correctly on Windows
-- [bugfix] Magnified blocks were not showing correct on startup
-- [bugfix] Window opacity and background color was not getting applied properly in all cases
-- [bugfix] Fix terminal theming when applying global defaults [#1287](https://github.com/wavetermdev/waveterm/issues/1287)
-- MacOS 10.15 (Catalina) is no longer supported
-- Other bug fixes, docs improvements, and dependency bumps
-
-### v0.9.2 — Nov 11, 2024
-
-New minor release with bug fixes and new features! Fixed the bug around making Wave fullscreen (also affecting certain window managers like Hyprland). We've also put a lot of work into the doc site (https://docs.waveterm.dev), including documenting how [Widgets](./widgets) and Presets work!
-
-- Updated documentation
-- Wave AI now supports the Anthropic API! Checkout the [FAQ](./faq) for how to use the Claude models with Wave AI.
-- Removed defaultwidgets.json and unified it to widgets.json. Makes it more straightforward to override the default widgets.
-- New resolvers for `-b` param in `wsh`. "tab:N" for accessing the nth tab, "[view]" and "[view]:N" for accessing blocks of a particlar view.
-- New `wsh ai` command to send AI chats (and files) directly to a new or existing AI block
-- wsh setmeta/getmeta improvements. Allow setmeta to take a json file (and also read from stdin), also better output formats for getmeta (compatible with setmeta).
-- [bugfix] Set max completion tokens in the OpenAI API so we can now work with o1 models (also fallback to non-streaming mode)
-- [bugfix] Fixed content resizing when entering "full screen" mode. This bug also affected certain window managers (like Hyprland)
-- Lots of other small bug fixes, docs updates, and dependency bumps
-
-### v0.9.1 — Nov 1, 2024
-
-Minor bug fix release to follow-up on the v0.9.0 build. Lots of issues fixed (especially for Windows).
-
-- CLI applications that need microphone, camera, or location access will now work on MacOS. You'll see a security popup in Wave to allow/deny [#1086](https://github.com/wavetermdev/waveterm/issues/1086)
-- Can now use `wsh version -v` to print out the new data/config directories
-- Restores the old T1, T2, T3, ... tab naming logic
-- Temporarily revert to using the "Title Bar" on windows to mitgate a bug where the window controls were overlaying on top of our tabs (working on a real fix for the next release)
-- There is a new setting in the editor to enable/disable word wrapping [#1038](https://github.com/wavetermdev/waveterm/issues/1038)
-- Ctrl-S will now save files in codeedit [#1081](https://github.com/wavetermdev/waveterm/issues/1081)
-- [#1020](https://github.com/wavetermdev/waveterm/issues/1020) there is now a preset config option to change the active border color in tab themes
-- [bugfix] Multiple fixes for [#1167](https://github.com/wavetermdev/waveterm/issues/1167) to try to address tab loss while updating
-- [bugfix] Windows app crashed on opening View menu because of a bad accelerator key
-- [bugfix] The auto-updater messages in the tab bar are now more consistent when switching tabs, and we don't show errors when the network is disconnected
-- [bugfix] Full-screen mode now actually shows tabs in full screen
-- [bugfix] [#1175](https://github.com/wavetermdev/waveterm/issues/1175) can now edit .awk files
-- [bugfix] [#1066](https://github.com/wavetermdev/waveterm/issues/1066) applying a default theme now updates the background appropriately without a refresh
-
-### v0.9.0 — Oct 28, 2024
-
-New major Wave Terminal release! Wave tabs are now cached. Tab switching performance is
-now much faster and webview state, editor state, and scroll positions are now persisted
-across tab changes. We also have native WSL2 support. You can create native Wave connections
-to your Windows WSL2 distributions using the connection button.
-
-We've also laid the groundwork for some big features that will be released over the
-next couple of weeks, including Workspaces, AI improvments, and custom widgets.
-
-Lots of other smaller changes and bug fixes. See full list of PRs at https://github.com/wavetermdev/waveterm/releases/tag/v0.9.0
-
-### v0.8.13 — Oct 24, 2024
-
-- Wave is now available as a Snap for Linux users! You can find it [in the Snap Store](https://snapcraft.io/waveterm).
-- Wave is now available via the Windows Package Manager! You can install it via `winget install CommandLine.Wave`
-- can now use "term:fontsize" to override an individual terminal block's font size (also in context menu)
-- we now allow mixed case hostnames for connections to be compatible with ssh config
-- The Linux app icon is now updated to match the Windows icon
-- [bugfix] fixed a bug that sometimes caused escape sequences to be printed when switching between tabs
-- [bugfix] fixed an issue where the preview block was not cleaning up temp files (Windows only)
-- [bugfix] fixed chrome sandbox permissions errors in linux
-- [bugfix] fixed shutdown logic on MacOS/Linux which sometimes allowed orphaned processes to survive
-
-### v0.8.12 — Oct 18, 2024
-
-- Added support for multiple AI configurations! You can now run Open AI side-by-side with Ollama models. Can create AI presets in presets.json, and can easily switch between them using a new dropdown in the AI widget
-- Fix WebSocket reconnection error. this sometimes caused the terminal to hang when waking up from sleep
-- Added memory graphs, and per-CPU graphs to the sysinfo widget (and renamed it from cpuplot)
-- Added a new huge red "Config Error" button when there are parse errors in the config JSON file
-- Preview/CodeEdit widget now shows errors (squiggly lines) when JSON or YAML files fail to parse
-- New app icon for Windows to better match Fluent UI standards
-- Added copy-on-select to the terminal (on by default, can disable using "term:copyonselect")
-- Added a button to mute audio in webviews
-- Added a right-click "Open Clipboard URL" to easily open a webview from an URL stored in your system clipboard
-- [bugfix] fixed blank "help" pages when waking from sleep or restarting the app
-
-### v0.8.11 — Oct 10, 2024
-
-Hotfix release to address a couple of bugs introduced in v0.8.10
-
-- Fixes a regression in v0.8.10 which caused new tabs to sometimes come up blank and broken
-- Layout fixes to the AI widget spacing
-- Terminal scrollbar is now semi-transparent and overlays last column
-- Fixes initial window size (on first startup) for both smaller and larger screens
-- Added a "Don't Ask Again" checkbox for installing `wsh` on remote machines (sets a new config flag)
-- Prevent the app from downgrading when you install a beta build. Installing a beta-build will now switch you to the beta-update channel.
-
-### v0.8.10 — Oct 9, 2024
-
-Minor big fix release (but there are some new features).
-
-- added support for Azure AI [See FAQ](https://docs.waveterm.dev/faq#how-can-i-connect-to-azure-ai)
-- AI errors now appear in the chat
-- on MacOS, hitting "Space" in directorypreview will open selected file in Quick Look
-- [bugfix] fixed transparency settings
-- [bugfix] fixed issue with non-standard port numbers in connection dropdown
-- [bugfix] fixed issue with embedded docsite (returned 404 after refresh)
-
-### v0.8.9 — Oct 8, 2024
-
-Lots of bug fixes and new features!
-
-- New "help" view -- uses an embedded version of our doc site -- https://docs.waveterm.dev
-- [breaking] wsh getmeta, wsh setmeta, and wsh deleteblock now take a blockid using a `-b` parameter instead of as a positional parameter
-- allow metadata to override the block icon, header, and text (frame:title, frame:icon, and frame:text)
-- home button on web widget to return to the homepage, option to set a homepage default for the whole app or just for the given block
-- checkpoint the terminal less often to reduce frequency of output bug (still working on a full fix)
-- new terminal themes -- Warm Yellow, and One Dark Pro
-- we now support github flavored markdown alerts
-- `wsh notify` command to send a desktop notification
-- `wsh createblock` to create any block via the CLI
-- right click to "Save Image" in webview
-- `wsh edit` will now allow you to open new files (as long as the parent directly exists)
-- added 8 new fun tab background presets (right click on any tab and select "Backgrounds" to try them out)
-- [config] new config key "term:scrollback" to set the number of lines of scrollback for terminals. Use "-1" to set 0, max is 10000.
-- [config] new config key "term:theme" to set the default terminal theme for all new terminals
-- [config] new config key "preview:showhiddenfiles" to set the default "show hidden files" setting for preview
-- [bugfix] fixed an formatting issue with `wsh getmeta`
-- [bugfix] fix for startup issue on Linux when home directory is an NFS mount
-- [bugfix] fix cursor color in terminal themes to work
-- [bugfix] fix some double scrollbars when showing markdown content
-- [bugfix] improved shutdown sequence to better capture wavesrv logs
-- [bugfix] fix Alt+G keyboard accelerator for Linux/Windows
-- other assorted bug fixes, cleanups, and security fixes
-
-### v0.8.8 — Oct 1, 2024
-
-Quick patch release to fix Windows/Linux "Alt" keybindings. Also brings a huge performance improvement to AI streaming speed.
-
-### v0.8.7 — Sep 30, 2024
-
-Quick patch release to fix bugs:
-
-- Fixes windows SSH connections (invalid path while trying to install wsh tools)
-- Fixes an issue resolving `~` in windows paths `~\` now works instead of just `~/`
-- Tries to fix background color for webpages. Pulls meta tag for color-scheme, and sets a black background if dark detected (fixes issue rendering raw githubusercontent files)
-- Fixed our useDimensions hook to fire correctly. Fixes some sizing issues including allowing error messages to show consistently when SSH connections fail.
-- Allow "data:" urls in custom tab backgrounds
-- All the alias "tab" for the current tab's UUID when using wsh
-- [BUILD] conditional write generated files only if they are updated
-
-### v0.8.6 — Sep 26, 2024
-
-Another quick hotfix update. Fixes an issue where, if you deleted all of the tabs in a window, the window would be restored on next startup as completely blank.
-
-Also, as a bonus, we added fish shell support!
-
-### v0.8.5 — Sep 25, 2024
-
-Hot fix, dowgrade `jotai` library. Upgrading caused a major regression in codeedit which did not allow
-users to edit files.
-
-### v0.8.4 — Sep 25, 2024
-
-- Added a setting `window:disablehardwareacceleration` to disable native hardware acceleration
-- New startup model for legacy users given them the option to download the WaveLegacy
-- Use WAVETERM_HOME for the home directory consistently
-
-### v0.8.3 — Sep 25, 2024
-
-More hotfixes for Linux users. We now link against an older version of glibc and use
-the zig compiler on linux (the newer version caused us not to run on older distros).
-Also fixes a permissions issue when installing via .deb. There is also a new config value
-`window:nativetitlebar` which restores the native titlebar on windows/linux.
-
-### v0.8.2 — Sep 24, 2024
-
-Hot fix, fixes a nasty crash on startup for Linux users (dynamic linking but with netcgo DNS library)
-
-### v0.8.1 — Sep 23, 2024
-
-Minor cleanup release.
-
-- fix number parsing for certain config file values
-- add link to docs site
-- add new back button for directory view
-- telemetry fixes
-
-### v0.8.0 — Sep 20, 2024
-
-**Major New Release of Wave Terminal**
-
-The new build is a fresh start, and a clean break from the current version. As such, your history, settings, and configuration will not be carried over. If you'd like to continue to run the legacy version, you will need to download it separately.
-
-Release Artificats and source code diffs can be found on (Github)[https://github.com/wavetermdev/waveterm].
diff --git a/docs/docs/telemetry-old.mdx b/docs/docs/telemetry-old.mdx
deleted file mode 100644
index dba263dacb..0000000000
--- a/docs/docs/telemetry-old.mdx
+++ /dev/null
@@ -1,130 +0,0 @@
----
-id: "telemetry-old"
-title: "Legacy Telemetry"
-sidebar_class_name: hidden
----
-
-Wave Terminal collects telemetry data to help us track feature use, direct future product efforts, and generate aggregate metrics on Wave's popularity and usage. We do not collect or store any PII (personal identifiable information) and all metric data is only associated with and aggregated using your randomly generated _ClientId_. You may opt out of collection at any time.
-
-If you would like to turn telemetry on or off, the first opportunity is a button on the initial welcome page. After this, it can be turned off by adding `"telemetry:enabled": false` to the `config/settings.json` file. It can alternatively be turned on by adding `"telemetry:enabled": true` to the `config/settings.json` file.
-
-:::info
-
-You can also change your telemetry setting by running the wsh command:
-
-```
-wsh setconfig telemetry:enabled=true
-```
-
-:::
-
----
-
-## Sending Telemetry
-
-Provided that telemetry is enabled, it is sent 10 seconds after Waveterm is first booted and then again every 4 hours thereafter. It can also be sent in response to a few special cases listed below. When telemetry is sent, it is grouped into individual days as determined by your time zone. Any data from a previous day is marked as `Uploaded` so it will not need to be sent again.
-
-### Sending Once Telemetry is Enabled
-
-As soon as telemetry is enabled, a telemetry update is sent regardless of how long it has been since the last send. This does not reset the usual timer for telemetry sends.
-
-### Notifying that Telemetry is Disabled
-
-As soon as telemetry is disabled, Waveterm sends a special update that notifies us of this change. See [When Telemetry is Turned Off](#when-telemetry-is-turned-off) for more info. The timer still runs in the background but no data is sent.
-
-### When Waveterm is Closed
-
-Provided that telemetry is enabled, it will be sent when Waveterm is closed.
-
----
-
-## Telemetry Data
-
-When telemetry is active, we collect the following data. It is stored in the `telemetry.TelemetryData` type in the source code.
-
-| Name | Description |
-| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| ActiveMinutes | The number of minutes that the user has actively used Waveterm on a given day. This requires the terminal window to be in focus while the user is actively interacting with it. |
-| FgMinutes | The number of minutes that Waveterm has been in the foreground on a given day. This requires the terminal window to be in focus regardless of user interaction. |
-| OpenMinutes | The number of minutes that Waveterm has been open on a given day. This only requires that the terminal is open, even if the window is out of focus. |
-| NumBlocks | The number of existing blocks open on a given day |
-| NumTabs | The number of existing tabs open on a given day. |
-| NewTab | The number of new tabs created on a given day |
-| NumWindows | The number of existing windows open on a given day. |
-| NumWS | The number of existing workspaces on a given day. |
-| NumWSNamed | The number of named workspaces on a give day. |
-| NewTab | The number of new tabs opened on a given day. |
-| NumStartup | The number of times waveterm has been started on a given day. |
-| NumShutdown | The number of times waveterm has been shut down on a given day. |
-| SetTabTheme | The number of times the tab theme is changed from the context menu |
-| NumMagnify | The number of times any block is magnified |
-| NumPanics | The number of backend (golang) panics caught in the current day |
-| NumAIReqs | The number of AI requests made in the current day |
-| NumSSHConn | The number of distinct SSH connections that have been made to distinct hosts |
-| NumWSLConns | The number of distinct WSL connections that have been made to distinct distros |
-| Renderers | The number of new block views of each type are open on a given day. |
-| WshCmds | The number of wsh commands of each type run on a given day |
-| Blocks | The number of blocks of different view types open on a given day |
-| Conn | The number of successful remote connections made (and errors) on a given day |
-
-## Associated Data
-
-In addition to the telemetry data collected, the following is also reported. It is stored in the `telemetry.ActivityType` type in the source code.
-
-| Name | Description |
-| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| Day | The date the telemetry is associated with. It does not include the time. |
-| Uploaded | A boolean that indicates if the telemetry for this day is finalized. It is false during the day the telemetry is associated with, but gets set true at the first telemetry upload after that. Once it is true, the data for that particular day will not be sent up with the telemetry any more. |
-| TzName | The code for the timezone the user's OS is reporting (e.g. PST, GMT, JST) |
-| TzOffset | The offset for the timezone the user's OS is reporting (e.g. -08:00, +00:00, +09:00) |
-| ClientVersion | Which version of Waveterm is installed. |
-| ClientArch | This includes the user's operating system (e.g. linux or darwin) and architecture (e.g. x86_64 or arm64). It does not include data for any Connections at this time. |
-| BuildTime | This serves as a more accurate version number that keeps track of when we built the version. It has no bearing on when that version was installed by you. |
-| OSRelease | This lists the version of the operating system the user has installed. |
-| Displays | Display resolutions (added in v0.9.3 to help us understand what screen resolutions to optimize for) |
-
-## Telemetry Metadata
-
-Lastly, some data is sent along with the telemetry that describes how to classify it. It is stored in the `wcloud.TelemetryInputType` in the source code.
-
-| Name | Description |
-| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
-| UserId | Currently Unused. This is an anonymous UUID intended for use in future features. |
-| ClientId | This is an anonymous UUID created when Waveterm is first launched. It is used for telemetry and sending prompts to Open AI. |
-| AppType | This is used to differentiate the current version of waveterm from the legacy app. |
-| AutoUpdateEnabled | Whether or not auto update is turned on. |
-| AutoUpdateChannel | The type of auto update in use. This specifically refers to whether a latest or beta channel is selected. |
-| CurDay | The current day (in your time zone) when telemetry is sent. It does not include the time of day. |
-
-## Geo Data
-
-We do not store IP addresses in our telemetry table. However, CloudFlare passes us Geo-Location headers. We store these two header values:
-
-| Name | Description |
-| ------------ | ----------------------------------------------------------------- |
-| CFCountry | 2-letter country code (e.g. "US", "FR", or "JP") |
-| CFRegionCode | region code (often a provence, region, or state within a country) |
-
----
-
-## When Telemetry is Turned Off
-
-When a user disables telemetry, Waveterm sends a notification that their anonymous _ClientId_ has had its telemetry disabled. This is done with the `wcloud.NoTelemetryInputType` type in the source code. Beyond that, no further information is sent unless telemetry is turned on again. If it is turned on again, the previous 30 days of telemetry will be sent.
-
----
-
-## A Note on IP Addresses
-
-Telemetry is uploaded via https, which means your IP address is known to the telemetry server. We **do not** store your IP address in our telemetry table and **do not** associate it with your _ClientId_.
-
----
-
-## Previously Collected Telemetry Data
-
-While we believe the data we collect with telemetry is fairly minimal, we cannot make that decision for every user. If you ever change your mind about what has been collected previously, you may request that your data be deleted by emailing us at [support@waveterm.dev](mailto:support@waveterm.dev). If you do, we will need your _ClientId_ to remove it.
-
----
-
-## Privacy Policy
-
-For a summary of the above, you can take a look at our [Privacy Policy](https://www.waveterm.dev/privacy).
diff --git a/docs/docs/telemetry.mdx b/docs/docs/telemetry.mdx
deleted file mode 100644
index 2f9132276d..0000000000
--- a/docs/docs/telemetry.mdx
+++ /dev/null
@@ -1,71 +0,0 @@
----
-sidebar_position: 100
-title: Telemetry
-id: "telemetry"
----
-
-## tl;dr
-
-Wave Terminal collects telemetry data to help us track feature use, direct future product efforts, and generate aggregate metrics on Wave's popularity and usage. We do NOT collect personal information (PII), keystrokes, file contents, AI prompts, IP addresses, hostnames, or commands. We attach all information to an anonymous, randomly generated _ClientId_ (UUID). You may opt out of collection at any time.
-
-Here's a quick summary of what is collected:
-
-- Basic App/System Info - OS, architecture, app version, update settings
-- Usage Metrics - App start/shutdown, active minutes, foreground time, tab/block counts/usage
-- Feature Interactions - When you create tabs, run commands, change settings, etc.
-- Display Info - Monitor resolution, number of displays
-- Connection Events - SSH/WSL connection attempts (but NOT hostnames/IPs)
-- Wave AI Usage - Model/provider selection, token counts, request metrics, latency (but NOT prompts or responses)
-- Error Reports - Crash/panic events with minimal debugging info, but no stack traces or detailed errors
-
-Telemetry can be disabled at any time in settings. If not disabled it is sent on startup, on shutdown, and every 4-hours.
-
-## How to Disable Telemetry
-
-Telemetry can be enabled or disabled on the initial welcome screen when Wave first starts. After setup, telemetry can be disabled by setting the `telemetry:enabled` key to `false` in Wave’s general configuration file. It can also be disabled using the CLI command `wsh setconfig telemetry:enabled=false`.
-
-:::info
-
-This document outlines the current telemetry system as of v0.11.1. As of v0.12.5, Wave Terminal no longer sends legacy telemetry. The previous telemetry documentation can be found in our [Legacy Telemetry Documentation](./telemetry-old.mdx) for historical reference.
-
-:::
-
-## Diagnostics Ping
-
-Wave sends a small, anonymous diagnostics ping after the app has been running for a short time and at most once per day thereafter. This is used to estimate active installs and understand which versions are still in use, so we can make informed decisions about ongoing support and deprecations.
-
-The ping includes only: your Wave version, OS/CPU arch, local date (yyyy-mm-dd, no timezone or clock time), your randomly generated anonymous client ID, and whether usage telemetry is enabled or disabled.
-
-It does not include usage data, commands, files, or any telemetry events.
-
-This ping is intentionally separate from telemetry so Wave can count active installs. If you'd like to disable it, set the WAVETERM_NOPING environment variable.
-
-## Sending Telemetry
-
-Provided that telemetry is enabled, it is sent shortly after Wave is first launched and then again every 4 hours thereafter. It can also be sent in response to a few special cases listed below. When telemetry is sent, events are marked as sent to prevent duplicate transmissions.
-
-### Sending Once Telemetry is Enabled
-
-As soon as telemetry is enabled, a telemetry update is sent regardless of how long it has been since the last send. This does not reset the usual timer for telemetry sends.
-
-### When Wave is Closed
-
-Provided that telemetry is enabled, it will be sent when Waveterm is closed.
-
-## Event Types and Properties
-
-Wave collects the event types and properties described in the summary above. As we add features, new events and properties may be added to track their usage.
-
-For the complete, current list of all telemetry events and properties, see the source code: [telemetrydata.go](https://github.com/wavetermdev/waveterm/blob/main/pkg/telemetry/telemetrydata/telemetrydata.go)
-
-## GDPR Opt-Out Compliance
-
-When telemetry is disabled, Wave sends a single minimal opt-out record associated with the anonymous client ID, recording that telemetry was turned off and when it occurred. This record is retained for compliance purposes. After that, no telemetry or usage data is sent.
-
-## Deleting Your Data
-
-If you want your previously collected telemetry data deleted, email us at support (at) waveterm.dev with your _ClientId_ and we'll remove it.
-
-## Privacy Policy
-
-For a summary of the above, you can take a look at our [Privacy Policy](https://www.waveterm.dev/privacy).
diff --git a/docs/docs/waveai-modes.mdx b/docs/docs/waveai-modes.mdx
deleted file mode 100644
index 62045b86a9..0000000000
--- a/docs/docs/waveai-modes.mdx
+++ /dev/null
@@ -1,565 +0,0 @@
----
-sidebar_position: 1.6
-id: "waveai-modes"
-title: "Wave AI (Local Models + BYOK)"
----
-
-import { VersionBadge } from "@site/src/components/versionbadge";
-
-
-
-Wave AI supports custom AI modes that allow you to use local models, custom API endpoints, and alternative AI providers. This gives you complete control over which models and providers you use with Wave's AI features.
-
-## Configuration Overview
-
-AI modes are configured in `~/.config/waveterm/waveai.json`.
-
-**To edit using the UI:**
-1. Click the settings (gear) icon in the widget bar
-2. Select "Settings" from the menu
-3. Choose "Wave AI Modes" from the settings sidebar
-
-**Or launch from the command line:**
-```bash
-wsh editconfig waveai.json
-```
-
-Each mode defines a complete AI configuration including the model, API endpoint, authentication, and display properties.
-
-## Provider-Based Configuration
-
-Wave AI now supports provider-based configuration which automatically applies sensible defaults for common providers. By specifying the `ai:provider` field, you can significantly simplify your configuration as the system will automatically set up endpoints, API types, and secret names.
-
-### Supported Providers
-
-- **`openai`** - OpenAI API (automatically configures endpoint and secret name) [[see example](#openai)]
-- **`openrouter`** - OpenRouter API (automatically configures endpoint and secret name) [[see example](#openrouter)]
-- **`nanogpt`** - NanoGPT API (automatically configures endpoint and secret name) [[see example](#nanogpt)]
-- **`groq`** - Groq API (automatically configures endpoint and secret name) [[see example](#groq)]
-- **`google`** - Google AI (Gemini) [[see example](#google-ai-gemini)]
-- **`azure`** - Azure OpenAI Service (modern API) [[see example](#azure-openai-modern-api)]
-- **`azure-legacy`** - Azure OpenAI Service (legacy deployment API) [[see example](#azure-openai-legacy-deployment-api)]
-- **`custom`** - Custom API endpoint (fully manual configuration) [[see examples](#local-model-examples)]
-
-### Supported API Types
-
-Wave AI supports the following API types:
-
-- **`openai-chat`**: Uses the `/v1/chat/completions` endpoint (most common)
-- **`openai-responses`**: Uses the `/v1/responses` endpoint (modern API for GPT-5+ models)
-- **`google-gemini`**: Google's Gemini API format (automatically set when using `ai:provider: "google"`, not typically used directly)
-
-## Global Wave AI Settings
-
-You can configure global Wave AI behavior in your Wave Terminal settings (separate from the mode configurations in `waveai.json`).
-
-### Setting a Default AI Mode
-
-After configuring a local model or custom mode, you can make it the default by setting `waveai:defaultmode` in your Wave Terminal settings.
-
-:::important
-Use the **mode key** (the key in your `waveai.json` configuration), not the display name. For example, use `"ollama-llama"` (the key), not `"Ollama - Llama 3.3"` (the display name).
-:::
-
-**Using the settings command:**
-```bash
-wsh setconfig waveai:defaultmode="ollama-llama"
-```
-
-**Or edit settings.json directly:**
-1. Click the settings (gear) icon in the widget bar
-2. Select "Settings" from the menu
-3. Add the `waveai:defaultmode` key to your settings.json:
-```json
- "waveai:defaultmode": "ollama-llama"
-```
-
-This will make the specified mode the default selection when opening Wave AI features.
-
-:::note
-Wave AI normally requires telemetry to be enabled. However, if you configure your own custom model (local or BYOK) and set `waveai:defaultmode` to that custom mode's key, you will not receive telemetry requirement messages. This allows you to use Wave AI features completely privately with your own models.
-:::
-
-### Hiding Wave Cloud Modes
-
-If you prefer to use only your local or custom models and want to hide Wave's cloud AI modes from the mode dropdown, set `waveai:showcloudmodes` to `false`:
-
-**Using the settings command:**
-```bash
-wsh setconfig waveai:showcloudmodes=false
-```
-
-**Or edit settings.json directly:**
-1. Click the settings (gear) icon in the widget bar
-2. Select "Settings" from the menu
-3. Add the `waveai:showcloudmodes` key to your settings.json:
-```json
- "waveai:showcloudmodes": false
-```
-
-This will hide Wave's built-in cloud AI modes, showing only your custom configured modes.
-
-## Local Model Examples
-
-### Ollama
-
-[Ollama](https://ollama.ai) provides an OpenAI-compatible API for running models locally:
-
-```json
-{
- "ollama-llama": {
- "display:name": "Ollama - Llama 3.3",
- "display:order": 1,
- "display:icon": "microchip",
- "display:description": "Local Llama 3.3 70B model via Ollama",
- "ai:apitype": "openai-chat",
- "ai:model": "llama3.3:70b",
- "ai:thinkinglevel": "medium",
- "ai:endpoint": "http://localhost:11434/v1/chat/completions",
- "ai:apitoken": "ollama"
- }
-}
-```
-
-:::tip
-The `ai:apitoken` field is required but Ollama ignores it - you can set it to any value like `"ollama"`.
-:::
-
-### LM Studio
-
-[LM Studio](https://lmstudio.ai) provides a local server that can run various models:
-
-```json
-{
- "lmstudio-qwen": {
- "display:name": "LM Studio - Qwen",
- "display:order": 2,
- "display:icon": "server",
- "display:description": "Local Qwen model via LM Studio",
- "ai:apitype": "openai-chat",
- "ai:model": "qwen/qwen-2.5-coder-32b-instruct",
- "ai:thinkinglevel": "medium",
- "ai:endpoint": "http://localhost:1234/v1/chat/completions",
- "ai:apitoken": "not-needed"
- }
-}
-```
-
-### vLLM
-
-[vLLM](https://docs.vllm.ai) is a high-performance inference server with OpenAI API compatibility:
-
-```json
-{
- "vllm-local": {
- "display:name": "vLLM",
- "display:order": 3,
- "display:icon": "server",
- "display:description": "Local model via vLLM",
- "ai:apitype": "openai-chat",
- "ai:model": "your-model-name",
- "ai:thinkinglevel": "medium",
- "ai:endpoint": "http://localhost:8000/v1/chat/completions",
- "ai:apitoken": "not-needed"
- }
-}
-```
-
-## Cloud Provider Examples
-
-### OpenAI
-
-Using the `openai` provider automatically configures the endpoint and secret name:
-
-```json
-{
- "openai-gpt4o": {
- "display:name": "GPT-4o",
- "ai:provider": "openai",
- "ai:model": "gpt-4o"
- }
-}
-```
-
-The provider automatically sets:
-- `ai:endpoint` to `https://api.openai.com/v1/chat/completions`
-- `ai:apitype` to `openai-chat` (or `openai-responses` for GPT-5+ models)
-- `ai:apitokensecretname` to `OPENAI_KEY` (store your OpenAI API key with this name)
-- `ai:capabilities` to `["tools", "images", "pdfs"]` (automatically determined based on model)
-
-For newer models like GPT-4.1 or GPT-5, the API type is automatically determined:
-
-```json
-{
- "openai-gpt41": {
- "display:name": "GPT-4.1",
- "ai:provider": "openai",
- "ai:model": "gpt-4.1"
- }
-}
-```
-
-### OpenAI Compatible
-
-To use an OpenAI compatible API provider, you need to provide the ai:endpoint, ai:apitokensecretname, ai:model parameters,
-and use "openai-chat" as the ai:apitype.
-
-:::note
-The ai:endpoint is *NOT* a baseurl. The endpoint should contain the full endpoint, not just the baseurl.
-For example: https://api.x.ai/v1/chat/completions
-
-If you provide only the baseurl, you are likely to get a 404 message.
-:::
-
-```json
-{
- "xai-grokfast": {
- "display:name": "xAI Grok Fast",
- "display:order": 2,
- "display:icon": "server",
- "ai:apitype": "openai-chat",
- "ai:model": "grok-4-1-fast-reasoning",
- "ai:endpoint": "https://api.x.ai/v1/chat/completions",
- "ai:apitokensecretname": "XAI_KEY",
- "ai:capabilities": ["tools", "images", "pdfs"]
- }
-}
-```
-
-The `ai:apitokensecretname` should be the name of an environment variable that contains your API key. Set this environment variable before running Wave Terminal.
-
-
-### OpenRouter
-
-[OpenRouter](https://openrouter.ai) provides access to multiple AI models. Using the `openrouter` provider simplifies configuration:
-
-```json
-{
- "openrouter-qwen": {
- "display:name": "OpenRouter - Qwen",
- "ai:provider": "openrouter",
- "ai:model": "qwen/qwen-2.5-coder-32b-instruct"
- }
-}
-```
-
-The provider automatically sets:
-- `ai:endpoint` to `https://openrouter.ai/api/v1/chat/completions`
-- `ai:apitype` to `openai-chat`
-- `ai:apitokensecretname` to `OPENROUTER_KEY` (store your OpenRouter API key with this name)
-
-:::note
-For OpenRouter, you must manually specify `ai:capabilities` based on your model's features. Example:
-```json
-{
- "openrouter-qwen": {
- "display:name": "OpenRouter - Qwen",
- "ai:provider": "openrouter",
- "ai:model": "qwen/qwen-2.5-coder-32b-instruct",
- "ai:capabilities": ["tools"]
- }
-}
-```
-:::
-
-### NanoGPT
-
-[NanoGPT](https://nano-gpt.com) provides access to multiple AI models at competitive prices. Using the `nanogpt` provider simplifies configuration:
-
-```json
-{
- "nanogpt-glm47": {
- "display:name": "NanoGPT - GLM 4.7",
- "ai:provider": "nanogpt",
- "ai:model": "zai-org/glm-4.7"
- }
-}
-```
-
-The provider automatically sets:
-- `ai:endpoint` to `https://nano-gpt.com/api/v1/chat/completions`
-- `ai:apitype` to `openai-chat`
-- `ai:apitokensecretname` to `NANOGPT_KEY` (store your NanoGPT API key with this name)
-
-:::note
-NanoGPT is a proxy service that provides access to multiple AI models. You must manually specify `ai:capabilities` based on the model's features. NanoGPT supports OpenAI-compatible tool calling for models that have that capability. Check the model's `capabilities.vision` field from the [NanoGPT models API](https://nano-gpt.com/api/v1/models?detailed=true) to determine image support. Example for a text-only model with tool support:
-```json
-{
- "nanogpt-glm47": {
- "display:name": "NanoGPT - GLM 4.7",
- "ai:provider": "nanogpt",
- "ai:model": "zai-org/glm-4.7",
- "ai:capabilities": ["tools"]
- }
-}
-```
-For vision-capable models like `openai/gpt-5`, add `"images"` to capabilities.
-:::
-
-### Groq
-
-[Groq](https://groq.com) provides fast inference for open models through an OpenAI-compatible API. Using the `groq` provider simplifies configuration:
-
-```json
-{
- "groq-kimi-k2": {
- "display:name": "Groq - Kimi K2",
- "ai:provider": "groq",
- "ai:model": "moonshotai/kimi-k2-instruct"
- }
-}
-```
-
-The provider automatically sets:
-- `ai:endpoint` to `https://api.groq.com/openai/v1/chat/completions`
-- `ai:apitype` to `openai-chat`
-- `ai:apitokensecretname` to `GROQ_KEY` (store your Groq API key with this name)
-
-:::note
-For Groq, you must manually specify `ai:capabilities` based on your model's features.
-:::
-
-### Google AI (Gemini)
-
-[Google AI](https://ai.google.dev) provides the Gemini family of models. Using the `google` provider simplifies configuration:
-
-```json
-{
- "google-gemini": {
- "display:name": "Gemini 3 Pro",
- "ai:provider": "google",
- "ai:model": "gemini-3-pro-preview"
- }
-}
-```
-
-The provider automatically sets:
-- `ai:endpoint` to `https://generativelanguage.googleapis.com/v1beta/models/{model}:streamGenerateContent`
-- `ai:apitype` to `google-gemini`
-- `ai:apitokensecretname` to `GOOGLE_AI_KEY` (store your Google AI API key with this name)
-- `ai:capabilities` to `["tools", "images", "pdfs"]` (automatically configured)
-
-### Azure OpenAI (Modern API)
-
-For the modern Azure OpenAI API, use the `azure` provider:
-
-```json
-{
- "azure-gpt4": {
- "display:name": "Azure GPT-4",
- "ai:provider": "azure",
- "ai:model": "gpt-4",
- "ai:azureresourcename": "your-resource-name"
- }
-}
-```
-
-The provider automatically sets:
-- `ai:endpoint` to `https://your-resource-name.openai.azure.com/openai/v1/chat/completions` (or `/responses` for newer models)
-- `ai:apitype` based on the model
-- `ai:apitokensecretname` to `AZURE_OPENAI_KEY` (store your Azure OpenAI key with this name)
-
-:::note
-For Azure providers, you must manually specify `ai:capabilities` based on your model's features. Example:
-```json
-{
- "azure-gpt4": {
- "display:name": "Azure GPT-4",
- "ai:provider": "azure",
- "ai:model": "gpt-4",
- "ai:azureresourcename": "your-resource-name",
- "ai:capabilities": ["tools", "images"]
- }
-}
-```
-:::
-
-### Azure OpenAI (Legacy Deployment API)
-
-For legacy Azure deployments, use the `azure-legacy` provider:
-
-```json
-{
- "azure-legacy-gpt4": {
- "display:name": "Azure GPT-4 (Legacy)",
- "ai:provider": "azure-legacy",
- "ai:azureresourcename": "your-resource-name",
- "ai:azuredeployment": "your-deployment-name"
- }
-}
-```
-
-The provider automatically constructs the full endpoint URL and sets the API version (defaults to `2025-04-01-preview`). You can override the API version with `ai:azureapiversion` if needed.
-
-:::note
-For Azure Legacy provider, you must manually specify `ai:capabilities` based on your model's features.
-:::
-
-## Using Secrets for API Keys
-
-Instead of storing API keys directly in the configuration, you should use Wave's secret store to keep your credentials secure. Secrets are stored encrypted using your system's native keychain.
-
-### Storing an API Key
-
-**Using the Secrets UI (recommended):**
-1. Click the settings (gear) icon in the widget bar
-2. Select "Secrets" from the menu
-3. Click "Add New Secret"
-4. Enter the secret name (e.g., `OPENAI_API_KEY`) and your API key
-5. Click "Save"
-
-**Or from the command line:**
-```bash
-wsh secret set OPENAI_KEY=sk-xxxxxxxxxxxxxxxx
-wsh secret set OPENROUTER_KEY=sk-xxxxxxxxxxxxxxxx
-```
-
-### Referencing the Secret
-
-When using providers like `openai` or `openrouter`, the secret name is automatically set. Just ensure the secret exists with the correct name:
-
-```json
-{
- "my-openai-mode": {
- "display:name": "OpenAI GPT-4o",
- "ai:provider": "openai",
- "ai:model": "gpt-4o"
- }
-}
-```
-
-The `openai` provider automatically looks for the `OPENAI_KEY` secret. See the [Secrets documentation](./secrets.mdx) for more information on managing secrets securely in Wave.
-
-## Multiple Modes Example
-
-You can define multiple AI modes and switch between them easily:
-
-```json
-{
- "ollama-llama": {
- "display:name": "Ollama - Llama 3.3",
- "display:order": 1,
- "ai:model": "llama3.3:70b",
- "ai:endpoint": "http://localhost:11434/v1/chat/completions",
- "ai:apitoken": "ollama"
- },
- "ollama-codellama": {
- "display:name": "Ollama - CodeLlama",
- "display:order": 2,
- "ai:model": "codellama:34b",
- "ai:endpoint": "http://localhost:11434/v1/chat/completions",
- "ai:apitoken": "ollama"
- },
- "openai-gpt4o": {
- "display:name": "GPT-4o",
- "display:order": 10,
- "ai:provider": "openai",
- "ai:model": "gpt-4o"
- }
-}
-```
-
-## Troubleshooting
-
-### Connection Issues
-
-If Wave can't connect to your model server:
-
-1. **For cloud providers with `ai:provider` set**: Ensure you have the correct secret stored (e.g., `OPENAI_KEY`, `OPENROUTER_KEY`)
-2. **For local/custom endpoints**: Verify the server is running (`curl http://localhost:11434/v1/models` for Ollama)
-3. Check the `ai:endpoint` is the complete endpoint URL including the path (e.g., `http://localhost:11434/v1/chat/completions`)
-4. Verify the `ai:apitype` matches your server's API (defaults are usually correct when using providers)
-5. Check firewall settings if using a non-localhost address
-
-### Model Not Found
-
-If you get "model not found" errors:
-
-1. Verify the model name matches exactly what your server expects
-2. For Ollama, use `ollama list` to see available models
-3. Some servers require prefixes or specific naming formats
-
-### API Type Selection
-
-- The API type defaults to `openai-chat` if not specified, which works for most providers
-- Use `openai-chat` for Ollama, LM Studio, custom endpoints, and most cloud providers
-- Use `openai-responses` for newer OpenAI models (GPT-5+) or when your provider specifically requires it
-- Provider presets automatically set the correct API type when needed
-
-## Configuration Reference
-
-### Minimal Configuration (with Provider)
-
-```json
-{
- "mode-key": {
- "display:name": "Qwen (OpenRouter)",
- "ai:provider": "openrouter",
- "ai:model": "qwen/qwen-2.5-coder-32b-instruct"
- }
-}
-```
-
-### Full Configuration (all fields)
-
-```json
-{
- "mode-key": {
- "display:name": "Display Name",
- "display:order": 1,
- "display:icon": "icon-name",
- "display:description": "Full description",
- "ai:provider": "custom",
- "ai:apitype": "openai-chat",
- "ai:model": "model-name",
- "ai:thinkinglevel": "medium",
- "ai:endpoint": "http://localhost:11434/v1/chat/completions",
- "ai:azureapiversion": "v1",
- "ai:apitoken": "your-token",
- "ai:apitokensecretname": "PROVIDER_KEY",
- "ai:azureresourcename": "your-resource",
- "ai:azuredeployment": "your-deployment",
- "ai:capabilities": ["tools", "images", "pdfs"]
- }
-}
-```
-
-### Field Reference
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `display:name` | Yes | Name shown in the AI mode selector |
-| `display:order` | No | Sort order in the selector (lower numbers first) |
-| `display:icon` | No | Icon identifier for the mode (can use any [FontAwesome icon](https://fontawesome.com/search), use the name without the "fa-" prefix). Default is "sparkles" |
-| `display:description` | No | Full description of the mode |
-| `ai:provider` | No | Provider preset: `openai`, `openrouter`, `nanogpt`, `groq`, `google`, `azure`, `azure-legacy`, `custom` |
-| `ai:apitype` | No | API type: `openai-chat`, `openai-responses`, or `google-gemini` (defaults to `openai-chat` if not specified) |
-| `ai:model` | No | Model identifier (required for most providers) |
-| `ai:thinkinglevel` | No | Thinking level: `low`, `medium`, or `high` |
-| `ai:endpoint` | No | *Full* API endpoint URL (auto-set by provider when available) |
-| `ai:azureapiversion` | No | Azure API version (for `azure-legacy` provider, defaults to `2025-04-01-preview`) |
-| `ai:apitoken` | No | API key/token (not recommended - use secrets instead) |
-| `ai:apitokensecretname` | No | Name of secret containing API token (auto-set by provider) |
-| `ai:azureresourcename` | No | Azure resource name (for Azure providers) |
-| `ai:azuredeployment` | No | Azure deployment name (for `azure-legacy` provider) |
-| `ai:capabilities` | No | Array of supported capabilities: `"tools"`, `"images"`, `"pdfs"` |
-| `waveai:cloud` | No | Internal - for Wave Cloud AI configuration only |
-| `waveai:premium` | No | Internal - for Wave Cloud AI configuration only |
-
-### AI Capabilities
-
-The `ai:capabilities` field specifies what features the AI mode supports:
-
-- **`tools`** - Enables AI tool usage for file reading/writing, shell integration, and widget interaction
-- **`images`** - Allows image attachments in chat (model can view uploaded images)
-- **`pdfs`** - Allows PDF file attachments in chat (model can read PDF content)
-
-**Provider-specific behavior:**
-- **OpenAI and Google providers**: Capabilities are automatically configured based on the model. You don't need to specify them.
-- **OpenRouter, NanoGPT, Groq, Azure, Azure-Legacy, and Custom providers**: You must manually specify capabilities based on your model's features.
-
-:::warning
-If you don't include `"tools"` in the `ai:capabilities` array, the AI model will not be able to interact with your Wave terminal widgets, read/write files, or execute commands. Most AI modes should include `"tools"` for the best Wave experience.
-:::
-
-Most models support `tools` and can benefit from it. Vision-capable models should include `images`. Not all models support PDFs, so only include `pdfs` if your model can process them.
diff --git a/docs/docs/waveai.mdx b/docs/docs/waveai.mdx
deleted file mode 100644
index 5189bc6792..0000000000
--- a/docs/docs/waveai.mdx
+++ /dev/null
@@ -1,110 +0,0 @@
----
-sidebar_position: 1.5
-id: "waveai"
-title: "Wave AI"
----
-
-import { Kbd } from "@site/src/components/kbd";
-import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext";
-
-
-
-
-
-
-Context-aware terminal assistant with access to terminal output, widgets, and filesystem.
-
-## Keyboard Shortcuts
-
-| Shortcut | Action |
-|----------|--------|
-| | Toggle AI panel |
-| | Focus AI input |
-| | Clear chat / start new |
-| | Send message |
-| | New line |
-
-## Widget Context Toggle
-
-Controls AI's access to your workspace:
-
-**ON**: AI can read terminal output, capture widget screenshots, access files/directories (with approval), navigate web widgets, and use custom widget tools. Use for debugging, code analysis, and workspace tasks.
-
-**OFF**: AI only sees your messages and attached files. Standard chat mode for general questions.
-
-## File Attachments
-
-Drag files onto the AI panel to attach (not supported with all models):
-
-| Type | Formats | Size Limit | Notes |
-|------|---------|------------|-------|
-| Images | JPEG, PNG, GIF, WebP, SVG | 10 MB | Auto-resized to 4096px max, converted to WebP |
-| PDFs | `.pdf` | 5 MB | Text extraction for analysis |
-| Text/Code | `.js`, `.ts`, `.py`, `.go`, `.md`, `.json`, `.yaml`, etc. | 200 KB | All common languages and configs |
-
-## CLI Integration
-
-Use `wsh ai` to send files and prompts from the command line:
-
-```bash
-git diff | wsh ai - # Pipe to AI
-wsh ai main.go -m "find bugs" # Attach files with message
-wsh ai $(tail -n 500 my.log) -m "review" -s # Auto-submit with output
-```
-
-Supports text files, images, PDFs, and directories. Use `-n` for new chat, `-s` to auto-submit.
-
-## AI Tools (Widget Context Enabled)
-
-### Terminal
-- **Read Terminal Output**: Fetches scrollback from terminal widgets, supports line ranges
-
-### File System
-- **Read Files**: Reads text files with line range support (requires approval)
-- **List Directories**: Returns file info, sizes, permissions, timestamps (requires approval)
-- **Write Text Files**: Create or modify files with diff preview and approval (requires approval)
-
-### Web
-- **Navigate Web**: Changes URLs in web browser widgets
-
-### All Widgets
-- **Capture Screenshots**: Takes screenshots of any widget for visual analysis (not supported on all models)
-
-:::warning Security
-File system operations require explicit approval. You control all file access.
-:::
-
-## Local Models & BYOK
-
-Wave AI supports using your own AI models and API keys:
-
-- **Local Models**: Run AI models locally with [Ollama](https://ollama.ai), [LM Studio](https://lmstudio.ai), [vLLM](https://docs.vllm.ai), and other OpenAI-compatible servers
-- **BYOK (Bring Your Own Key)**: Use your own API keys with OpenAI, OpenRouter, Google AI (Gemini), Azure OpenAI, and other cloud providers
-- **Multiple Modes**: Configure and switch between multiple AI providers and models
-- **Privacy**: Keep your data local or use your preferred cloud provider
-
-See the [**Local Models & BYOK guide**](./waveai-modes.mdx) for complete configuration instructions, examples, and troubleshooting.
-
-## Privacy
-
-**Default Wave AI Service:**
-- Messages are proxied through the Wave Cloud AI service (powered by OpenAI's APIs). Please refer to OpenAI's privacy policy for details on how they handle your data.
-- Wave does not store your chats, attachments, or use them for training
-- Usage counters included in anonymous telemetry
-- File access requires explicit approval
-
-**Local Models & BYOK:**
-- When using local models, your chat data never leaves your machine
-- When using BYOK with cloud providers, requests are sent directly to your chosen provider
-- Refer to your provider's privacy policy for details on how they handle your data
-
-:::info Under Active Development
-Wave AI is in active beta with included AI credits while we refine the experience. Share feedback in our [Discord](https://discord.gg/XfvZ334gwU).
-
-**Coming Soon:**
-- **Remote File Access**: Read files on SSH-connected systems
-- **Command Execution**: Run terminal commands with approval
-- **Web Content**: Extract text from web pages (currently screenshots only)
-:::
-
-
\ No newline at end of file
diff --git a/docs/docs/wsh-reference.mdx b/docs/docs/wsh-reference.mdx
index 6ed1bcaa3f..fb7247f91d 100644
--- a/docs/docs/wsh-reference.mdx
+++ b/docs/docs/wsh-reference.mdx
@@ -188,9 +188,6 @@ wsh editconfig presets.json
# opens widgets.json
wsh editconfig widgets.json
-
-# opens ai presets
-wsh editconfig presets/ai.json
```
---
@@ -990,7 +987,7 @@ Flags:
- `--workspace ` - restrict to specific workspace id
- `--window ` - restrict to specific window id
- `--tab ` - restrict to specific tab id
-- `--view ` - filter by view type (term, web, preview, edit, sysinfo, waveai)
+- `--view ` - filter by view type (term, web, preview, edit, sysinfo)
- `--json` - output results as JSON
- `--timeout ` - RPC timeout in milliseconds (default: 5000)
diff --git a/emain/emain-activity.ts b/emain/emain-activity.ts
index 17dde466ae..360d93217f 100644
--- a/emain/emain-activity.ts
+++ b/emain/emain-activity.ts
@@ -9,10 +9,6 @@ let globalIsStarting = true;
let globalIsRelaunching = false;
let forceQuit = false;
let userConfirmedQuit = false;
-let termCommandsRun = 0;
-let termCommandsRemote = 0;
-let termCommandsWsl = 0;
-let termCommandsDurable = 0;
export function setWasActive(val: boolean) {
wasActive = val;
@@ -66,42 +62,4 @@ export function getUserConfirmedQuit(): boolean {
return userConfirmedQuit;
}
-export function incrementTermCommandsRun() {
- termCommandsRun++;
-}
-
-export function getAndClearTermCommandsRun(): number {
- const count = termCommandsRun;
- termCommandsRun = 0;
- return count;
-}
-
-export function incrementTermCommandsRemote() {
- termCommandsRemote++;
-}
-
-export function getAndClearTermCommandsRemote(): number {
- const count = termCommandsRemote;
- termCommandsRemote = 0;
- return count;
-}
-export function incrementTermCommandsWsl() {
- termCommandsWsl++;
-}
-
-export function getAndClearTermCommandsWsl(): number {
- const count = termCommandsWsl;
- termCommandsWsl = 0;
- return count;
-}
-
-export function incrementTermCommandsDurable() {
- termCommandsDurable++;
-}
-
-export function getAndClearTermCommandsDurable(): number {
- const count = termCommandsDurable;
- termCommandsDurable = 0;
- return count;
-}
diff --git a/emain/emain-ipc.ts b/emain/emain-ipc.ts
index 5e5f15b302..618342104d 100644
--- a/emain/emain-ipc.ts
+++ b/emain/emain-ipc.ts
@@ -12,12 +12,7 @@ import { RpcApi } from "../frontend/app/store/wshclientapi";
import { getWebServerEndpoint } from "../frontend/util/endpoints";
import * as keyutil from "../frontend/util/keyutil";
import { fireAndForget, parseDataUrl } from "../frontend/util/util";
-import {
- incrementTermCommandsDurable,
- incrementTermCommandsRemote,
- incrementTermCommandsRun,
- incrementTermCommandsWsl,
- setWasActive,
+import { setWasActive,
} from "./emain-activity";
import { createBuilderWindow, getAllBuilderWindows, getBuilderWindowByWebContentsId } from "./emain-builder";
import { callWithOriginalXdgCurrentDesktopAsync, unamePlatform } from "./emain-platform";
@@ -438,22 +433,6 @@ export function initIpcHandlers() {
console.log("fe-log", logStr);
});
- electron.ipcMain.on(
- "increment-term-commands",
- (event, opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) => {
- incrementTermCommandsRun();
- if (opts?.isRemote) {
- incrementTermCommandsRemote();
- }
- if (opts?.isWsl) {
- incrementTermCommandsWsl();
- }
- if (opts?.isDurable) {
- incrementTermCommandsDurable();
- }
- }
- );
-
electron.ipcMain.on("native-paste", (event) => {
event.sender.paste();
});
diff --git a/emain/emain-tabview.ts b/emain/emain-tabview.ts
index 753a53adec..fa89df5317 100644
--- a/emain/emain-tabview.ts
+++ b/emain/emain-tabview.ts
@@ -118,7 +118,6 @@ export function getWaveTabViewByWebContentsId(webContentsId: number): WaveTabVie
export class WaveTabView extends WebContentsView {
waveWindowId: string; // this will be set for any tabviews that are initialized. (unset for the hot spare)
isActiveTab: boolean;
- isWaveAIOpen: boolean;
private _waveTabId: string; // always set, WaveTabViews are unique per tab
lastUsedTs: number; // ts milliseconds
createdTs: number; // ts milliseconds
@@ -142,7 +141,6 @@ export class WaveTabView extends WebContentsView {
},
});
this.createdTs = Date.now();
- this.isWaveAIOpen = false;
this.savedInitOpts = null;
this.initPromise = new Promise((resolve, _) => {
this.initResolve = resolve;
diff --git a/emain/emain-window.ts b/emain/emain-window.ts
index e3bfa87751..96bcade6a2 100644
--- a/emain/emain-window.ts
+++ b/emain/emain-window.ts
@@ -757,13 +757,6 @@ ipcMain.on("create-tab", async (event, _opts) => {
return null;
});
-ipcMain.on("set-waveai-open", (event, isOpen: boolean) => {
- const tabView = getWaveTabViewByWebContentsId(event.sender.id);
- if (tabView) {
- tabView.isWaveAIOpen = isOpen;
- }
-});
-
ipcMain.handle("close-tab", async (event, workspaceId: string, tabId: string, confirmClose: boolean) => {
const ww = getWaveWindowByWorkspaceId(workspaceId);
if (ww == null) {
diff --git a/emain/emain.ts b/emain/emain.ts
index 8b08178aec..c1c343fb9b 100644
--- a/emain/emain.ts
+++ b/emain/emain.ts
@@ -12,10 +12,6 @@ import { fireAndForget, sleep } from "../frontend/util/util";
import { AuthKey, configureAuthKeyRequestInjection } from "./authkey";
import {
getActivityState,
- getAndClearTermCommandsDurable,
- getAndClearTermCommandsRemote,
- getAndClearTermCommandsRun,
- getAndClearTermCommandsWsl,
getForceQuit,
getGlobalIsRelaunching,
getUserConfirmedQuit,
@@ -121,123 +117,8 @@ function handleWSEvent(evtMsg: WSEventType) {
});
}
-// we try to set the primary display as index [0]
-function getActivityDisplays(): ActivityDisplayType[] {
- const displays = electron.screen.getAllDisplays();
- const primaryDisplay = electron.screen.getPrimaryDisplay();
- const rtn: ActivityDisplayType[] = [];
- for (const display of displays) {
- const adt = {
- width: display.size.width,
- height: display.size.height,
- dpr: display.scaleFactor,
- internal: display.internal,
- };
- if (display.id === primaryDisplay?.id) {
- rtn.unshift(adt);
- } else {
- rtn.push(adt);
- }
- }
- return rtn;
-}
-
-async function sendDisplaysTDataEvent() {
- const displays = getActivityDisplays();
- if (displays.length === 0) {
- return;
- }
- const props: TEventProps = {};
- props["display:count"] = displays.length;
- props["display:height"] = displays[0].height;
- props["display:width"] = displays[0].width;
- props["display:dpr"] = displays[0].dpr;
- props["display:all"] = displays;
- try {
- await RpcApi.RecordTEventCommand(
- ElectronWshClient,
- {
- event: "app:display",
- props,
- },
- { noresponse: true }
- );
- } catch (e) {
- console.log("error sending display tdata event", e);
- }
-}
-
-function logActiveState() {
- fireAndForget(async () => {
- const astate = getActivityState();
- const activity: ActivityUpdate = { openminutes: 1 };
- const ww = focusedWaveWindow;
- const activeTabView = ww?.activeTabView;
- const isWaveAIOpen = activeTabView?.isWaveAIOpen ?? false;
-
- if (astate.wasInFg) {
- activity.fgminutes = 1;
- }
- if (astate.wasActive) {
- activity.activeminutes = 1;
- }
- activity.displays = getActivityDisplays();
-
- const termCmdCount = getAndClearTermCommandsRun();
- if (termCmdCount > 0) {
- activity.termcommandsrun = termCmdCount;
- }
- const termCmdRemoteCount = getAndClearTermCommandsRemote();
- const termCmdWslCount = getAndClearTermCommandsWsl();
- const termCmdDurableCount = getAndClearTermCommandsDurable();
-
- const props: TEventProps = {
- "activity:activeminutes": activity.activeminutes,
- "activity:fgminutes": activity.fgminutes,
- "activity:openminutes": activity.openminutes,
- };
- if (termCmdCount > 0) {
- props["activity:termcommandsrun"] = termCmdCount;
- }
- if (termCmdRemoteCount > 0) {
- props["activity:termcommands:remote"] = termCmdRemoteCount;
- }
- if (termCmdWslCount > 0) {
- props["activity:termcommands:wsl"] = termCmdWslCount;
- }
- if (termCmdDurableCount > 0) {
- props["activity:termcommands:durable"] = termCmdDurableCount;
- }
- if (astate.wasActive && isWaveAIOpen) {
- props["activity:waveaiactiveminutes"] = 1;
- }
- if (astate.wasInFg && isWaveAIOpen) {
- props["activity:waveaifgminutes"] = 1;
- }
-
- try {
- await RpcApi.ActivityCommand(ElectronWshClient, activity, { noresponse: true });
- await RpcApi.RecordTEventCommand(
- ElectronWshClient,
- {
- event: "app:activity",
- props,
- },
- { noresponse: true }
- );
- } catch (e) {
- console.log("error logging active state", e);
- } finally {
- setWasInFg(ww?.isFocused() ?? false);
- setWasActive(false);
- }
- });
-}
-
// this isn't perfect, but gets the job done without being complicated
-function runActiveTimer() {
- logActiveState();
- setTimeout(runActiveTimer, 60000);
+function runActiveTimer() { setTimeout(runActiveTimer, 60000);
}
function hideWindowWithCatch(window: WaveBrowserWindow) {
@@ -417,8 +298,6 @@ async function appMain() {
ensureHotSpareTab(fullConfig);
await relaunchBrowserWindows();
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
- setTimeout(sendDisplaysTDataEvent, 5000);
-
makeAndSetAppMenu();
makeDockTaskbar();
await configureAutoUpdater();
diff --git a/emain/preload.ts b/emain/preload.ts
index 8d2b18a308..e23dfec447 100644
--- a/emain/preload.ts
+++ b/emain/preload.ts
@@ -62,10 +62,7 @@ contextBridge.exposeInMainWorld("api", {
captureScreenshot: (rect: Rectangle) => ipcRenderer.invoke("capture-screenshot", rect),
setKeyboardChordMode: () => ipcRenderer.send("set-keyboard-chord-mode"),
clearWebviewStorage: (webContentsId: number) => ipcRenderer.invoke("clear-webview-storage", webContentsId),
- setWaveAIOpen: (isOpen: boolean) => ipcRenderer.send("set-waveai-open", isOpen),
closeBuilderWindow: () => ipcRenderer.send("close-builder-window"),
- incrementTermCommands: (opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) =>
- ipcRenderer.send("increment-term-commands", opts),
nativePaste: () => ipcRenderer.send("native-paste"),
openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId),
setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId),
diff --git a/frontend/app/aipanel/ai-utils.ts b/frontend/app/aipanel/ai-utils.ts
deleted file mode 100644
index 8bfd67bdc0..0000000000
--- a/frontend/app/aipanel/ai-utils.ts
+++ /dev/null
@@ -1,598 +0,0 @@
-// Copyright 2025, Command Line Inc.
-// SPDX-License-Identifier: Apache-2.0
-
-import { sortByDisplayOrder } from "@/util/util";
-
-const TextFileLimit = 200 * 1024; // 200KB
-const PdfLimit = 5 * 1024 * 1024; // 5MB
-const ImageLimit = 10 * 1024 * 1024; // 10MB
-const ImagePreviewSize = 128;
-const ImagePreviewWebPQuality = 0.8;
-const ImageMaxEdge = 4096;
-
-export const isAcceptableFile = (file: File): boolean => {
- const acceptableTypes = [
- // Images
- "image/jpeg",
- "image/jpg",
- "image/png",
- "image/gif",
- "image/webp",
- "image/svg+xml",
- // PDFs
- "application/pdf",
- // Text files
- "text/plain",
- "text/markdown",
- "text/html",
- "text/css",
- "text/javascript",
- "text/typescript",
- // Application types for code files
- "application/javascript",
- "application/typescript",
- "application/json",
- "application/xml",
- ];
-
- if (acceptableTypes.includes(file.type)) {
- return true;
- }
-
- // Check file extensions for files without proper MIME types
- const extension = file.name.split(".").pop()?.toLowerCase();
- const acceptableExtensions = [
- "txt",
- "log",
- "md",
- "js",
- "mjs",
- "cjs",
- "jsx",
- "ts",
- "mts",
- "cts",
- "tsx",
- "go",
- "py",
- "java",
- "c",
- "cpp",
- "h",
- "hpp",
- "html",
- "htm",
- "css",
- "scss",
- "sass",
- "json",
- "jsonc",
- "json5",
- "jsonl",
- "ndjson",
- "xml",
- "yaml",
- "yml",
- "sh",
- "bat",
- "sql",
- "php",
- "rb",
- "rs",
- "swift",
- "kt",
- "cs",
- "vb",
- "r",
- "scala",
- "clj",
- "ex",
- "exs",
- "ini",
- "toml",
- "conf",
- "cfg",
- "env",
- "zsh",
- "fish",
- "ps1",
- "psm1",
- "bazel",
- "bzl",
- "csv",
- "tsv",
- "properties",
- "ipynb",
- "rmd",
- "gradle",
- "groovy",
- "cmake",
- ];
-
- if (extension && acceptableExtensions.includes(extension)) {
- return true;
- }
-
- // Check for specific filenames (case-insensitive)
- const fileName = file.name.toLowerCase();
- const acceptableFilenames = [
- "makefile",
- "dockerfile",
- "containerfile",
- "go.mod",
- "go.sum",
- "go.work",
- "go.work.sum",
- "package.json",
- "package-lock.json",
- "yarn.lock",
- "pnpm-lock.yaml",
- "composer.json",
- "composer.lock",
- "gemfile",
- "gemfile.lock",
- "podfile",
- "podfile.lock",
- "cargo.toml",
- "cargo.lock",
- "pipfile",
- "pipfile.lock",
- "requirements.txt",
- "setup.py",
- "pyproject.toml",
- "poetry.lock",
- "build.gradle",
- "settings.gradle",
- "pom.xml",
- "build.xml",
- "readme",
- "readme.md",
- "license",
- "license.md",
- "changelog",
- "changelog.md",
- "contributing",
- "contributing.md",
- "authors",
- "codeowners",
- "procfile",
- "jenkinsfile",
- "vagrantfile",
- "rakefile",
- "gruntfile.js",
- "gulpfile.js",
- "webpack.config.js",
- "rollup.config.js",
- "vite.config.js",
- "jest.config.js",
- "vitest.config.js",
- ".dockerignore",
- ".gitignore",
- ".gitattributes",
- ".gitmodules",
- ".editorconfig",
- ".eslintrc",
- ".prettierrc",
- ".pylintrc",
- ".bashrc",
- ".bash_profile",
- ".bash_login",
- ".bash_logout",
- ".profile",
- ".zshrc",
- ".zprofile",
- ".zshenv",
- ".zlogin",
- ".zlogout",
- ".kshrc",
- ".cshrc",
- ".tcshrc",
- ".xonshrc",
- ".shrc",
- ".aliases",
- ".functions",
- ".exports",
- ".direnvrc",
- ".vimrc",
- ".gvimrc",
- ];
-
- return acceptableFilenames.includes(fileName);
-};
-
-export const getFileIcon = (fileName: string, fileType: string): string => {
- if (fileType === "directory") {
- return "fa-folder";
- }
-
- if (fileType.startsWith("image/")) {
- return "fa-image";
- }
-
- if (fileType === "application/pdf") {
- return "fa-file-pdf";
- }
-
- // Check file extensions for code files
- const ext = fileName.split(".").pop()?.toLowerCase();
- switch (ext) {
- case "js":
- case "jsx":
- case "ts":
- case "tsx":
- return "fa-file-code";
- case "go":
- return "fa-file-code";
- case "py":
- return "fa-file-code";
- case "java":
- case "c":
- case "cpp":
- case "h":
- case "hpp":
- return "fa-file-code";
- case "html":
- case "css":
- case "scss":
- case "sass":
- return "fa-file-code";
- case "json":
- case "xml":
- case "yaml":
- case "yml":
- return "fa-file-code";
- case "md":
- case "txt":
- return "fa-file-text";
- default:
- return "fa-file";
- }
-};
-
-export const formatFileSize = (bytes: number): string => {
- if (bytes === 0) return "0 B";
- const k = 1024;
- const sizes = ["B", "KB", "MB", "GB"];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
-};
-
-// Normalize MIME type for AI processing
-export const normalizeMimeType = (file: File): string => {
- const fileType = file.type;
-
- // Images keep their real mimetype
- if (fileType.startsWith("image/")) {
- return fileType;
- }
-
- // PDFs keep their mimetype
- if (fileType === "application/pdf") {
- return fileType;
- }
-
- // Everything else (code files, markdown, text, etc.) becomes text/plain
- return "text/plain";
-};
-
-// Helper function to read file as base64 for AIMessage
-export const readFileAsBase64 = (file: File): Promise => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => {
- const result = reader.result as string;
- // Remove data URL prefix to get just base64
- const base64 = result.split(",")[1];
- resolve(base64);
- };
- reader.onerror = reject;
- reader.readAsDataURL(file);
- });
-};
-
-// Helper function to create data URL for UIMessage
-export const createDataUrl = (file: File): Promise => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result as string);
- reader.onerror = reject;
- reader.readAsDataURL(file);
- });
-};
-
-export interface FileSizeError {
- fileName: string;
- fileSize: number;
- maxSize: number;
- fileType: "text" | "pdf" | "image";
-}
-
-export const validateFileSize = (file: File): FileSizeError | null => {
- if (file.type.startsWith("image/")) {
- if (file.size > ImageLimit) {
- return {
- fileName: file.name,
- fileSize: file.size,
- maxSize: ImageLimit,
- fileType: "image",
- };
- }
- } else if (file.type === "application/pdf") {
- if (file.size > PdfLimit) {
- return {
- fileName: file.name,
- fileSize: file.size,
- maxSize: PdfLimit,
- fileType: "pdf",
- };
- }
- } else {
- if (file.size > TextFileLimit) {
- return {
- fileName: file.name,
- fileSize: file.size,
- maxSize: TextFileLimit,
- fileType: "text",
- };
- }
- }
-
- return null;
-};
-
-export const validateFileSizeFromInfo = (
- fileName: string,
- fileSize: number,
- mimeType: string
-): FileSizeError | null => {
- let maxSize: number;
- let fileType: "text" | "pdf" | "image";
-
- if (mimeType.startsWith("image/")) {
- maxSize = ImageLimit;
- fileType = "image";
- } else if (mimeType === "application/pdf") {
- maxSize = PdfLimit;
- fileType = "pdf";
- } else {
- maxSize = TextFileLimit;
- fileType = "text";
- }
-
- if (fileSize > maxSize) {
- return {
- fileName,
- fileSize,
- maxSize,
- fileType,
- };
- }
-
- return null;
-};
-
-export const formatFileSizeError = (error: FileSizeError): string => {
- const typeLabel = error.fileType === "image" ? "Image" : error.fileType === "pdf" ? "PDF" : "Text file";
- return `${typeLabel} "${error.fileName}" is too large (${formatFileSize(error.fileSize)}). Maximum size is ${formatFileSize(error.maxSize)}.`;
-};
-
-/**
- * Resize an image to have a maximum edge of 4096px and convert to WebP format
- * Returns the optimized image if it's smaller than the original, otherwise returns the original
- */
-export const resizeImage = async (file: File): Promise => {
- // Only process actual image files (not SVG)
- if (!file.type.startsWith("image/") || file.type === "image/svg+xml") {
- return file;
- }
-
- return new Promise((resolve) => {
- const img = new Image();
- const url = URL.createObjectURL(file);
-
- img.onload = async () => {
- URL.revokeObjectURL(url);
-
- let { width, height } = img;
-
- // Check if resizing is needed
- if (width <= ImageMaxEdge && height <= ImageMaxEdge) {
- // Image is already small enough, just try WebP conversion
- const canvas = document.createElement("canvas");
- canvas.width = width;
- canvas.height = height;
- const ctx = canvas.getContext("2d");
- ctx?.drawImage(img, 0, 0);
-
- canvas.toBlob(
- (blob) => {
- if (blob && blob.size < file.size) {
- const webpFile = new File([blob], file.name.replace(/\.[^.]+$/, ".webp"), {
- type: "image/webp",
- });
- console.log(
- `Image resized (no dimension change): ${file.name} - Original: ${formatFileSize(file.size)}, WebP: ${formatFileSize(blob.size)}`
- );
- resolve(webpFile);
- } else {
- console.log(
- `Image kept original (WebP not smaller): ${file.name} - ${formatFileSize(file.size)}`
- );
- resolve(file);
- }
- },
- "image/webp",
- ImagePreviewWebPQuality
- );
- return;
- }
-
- // Calculate new dimensions while maintaining aspect ratio
- if (width > height) {
- height = Math.round((height * ImageMaxEdge) / width);
- width = ImageMaxEdge;
- } else {
- width = Math.round((width * ImageMaxEdge) / height);
- height = ImageMaxEdge;
- }
-
- // Create canvas and resize
- const canvas = document.createElement("canvas");
- canvas.width = width;
- canvas.height = height;
- const ctx = canvas.getContext("2d");
- ctx?.drawImage(img, 0, 0, width, height);
-
- // Convert to WebP
- canvas.toBlob(
- (blob) => {
- if (blob && blob.size < file.size) {
- const webpFile = new File([blob], file.name.replace(/\.[^.]+$/, ".webp"), {
- type: "image/webp",
- });
- console.log(
- `Image resized: ${file.name} (${img.width}x${img.height} → ${width}x${height}) - Original: ${formatFileSize(file.size)}, WebP: ${formatFileSize(blob.size)}`
- );
- resolve(webpFile);
- } else {
- console.log(
- `Image kept original (WebP not smaller): ${file.name} (${img.width}x${img.height} → ${width}x${height}) - ${formatFileSize(file.size)}`
- );
- resolve(file);
- }
- },
- "image/webp",
- ImagePreviewWebPQuality
- );
- };
-
- img.onerror = () => {
- URL.revokeObjectURL(url);
- resolve(file);
- };
-
- img.src = url;
- });
-};
-
-/**
- * Create a 128x128 preview data URL for an image file
- */
-export const createImagePreview = async (file: File): Promise => {
- if (!file.type.startsWith("image/") || file.type === "image/svg+xml") {
- return null;
- }
-
- return new Promise((resolve) => {
- const img = new Image();
- const url = URL.createObjectURL(file);
-
- img.onload = () => {
- URL.revokeObjectURL(url);
-
- let { width, height } = img;
-
- if (width > height) {
- height = Math.round((height * ImagePreviewSize) / width);
- width = ImagePreviewSize;
- } else {
- width = Math.round((width * ImagePreviewSize) / height);
- height = ImagePreviewSize;
- }
-
- const canvas = document.createElement("canvas");
- canvas.width = width;
- canvas.height = height;
- const ctx = canvas.getContext("2d");
- ctx?.drawImage(img, 0, 0, width, height);
-
- canvas.toBlob(
- (blob) => {
- if (blob) {
- const reader = new FileReader();
- reader.onloadend = () => {
- resolve(reader.result as string);
- };
- reader.readAsDataURL(blob);
- } else {
- resolve(null);
- }
- },
- "image/webp",
- ImagePreviewWebPQuality
- );
- };
-
- img.onerror = () => {
- URL.revokeObjectURL(url);
- resolve(null);
- };
-
- img.src = url;
- });
-};
-
-
-/**
- * Filter and organize AI mode configs into Wave and custom provider groups
- * Returns organized configs that should be displayed based on settings and premium status
- */
-export interface FilteredAIModeConfigs {
- waveProviderConfigs: Array<{ mode: string } & AIModeConfigType>;
- otherProviderConfigs: Array<{ mode: string } & AIModeConfigType>;
- shouldShowCloudModes: boolean;
-}
-
-export const getFilteredAIModeConfigs = (
- aiModeConfigs: Record,
- showCloudModes: boolean,
- inBuilder: boolean,
- hasPremium: boolean,
- currentMode?: string
-): FilteredAIModeConfigs => {
- const hideQuick = inBuilder && hasPremium;
-
- const allConfigs = Object.entries(aiModeConfigs)
- .map(([mode, config]) => ({ mode, ...config }))
- .filter((config) => !(hideQuick && config.mode === "waveai@quick"));
-
- const otherProviderConfigs = allConfigs
- .filter((config) => config["ai:provider"] !== "wave")
- .sort(sortByDisplayOrder);
-
- const hasCustomModels = otherProviderConfigs.length > 0;
- const isCurrentModeCloud = currentMode?.startsWith("waveai@") ?? false;
- const shouldShowCloudModes = showCloudModes || !hasCustomModels || isCurrentModeCloud;
-
- const waveProviderConfigs = shouldShowCloudModes
- ? allConfigs.filter((config) => config["ai:provider"] === "wave").sort(sortByDisplayOrder)
- : [];
-
- return {
- waveProviderConfigs,
- otherProviderConfigs,
- shouldShowCloudModes,
- };
-};
-
-/**
- * Get the display name for an AI mode configuration.
- * If display:name is set, use that. Otherwise, construct from model/provider.
- * For azure-legacy, show "azureresourcename (azure)".
- * For other providers, show "model (provider)".
- */
-export function getModeDisplayName(config: AIModeConfigType): string {
- if (config["display:name"]) {
- return config["display:name"];
- }
-
- const provider = config["ai:provider"];
- const model = config["ai:model"];
- const azureResourceName = config["ai:azureresourcename"];
-
- if (provider === "azure-legacy") {
- return `${azureResourceName || "unknown"} (azure)`;
- }
-
- return `${model || "unknown"} (${provider || "custom"})`;
-}
diff --git a/frontend/app/aipanel/aidroppedfiles.tsx b/frontend/app/aipanel/aidroppedfiles.tsx
deleted file mode 100644
index d7051c412f..0000000000
--- a/frontend/app/aipanel/aidroppedfiles.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2025, Command Line Inc.
-// SPDX-License-Identifier: Apache-2.0
-
-import { cn } from "@/util/util";
-import { useAtomValue } from "jotai";
-import { memo } from "react";
-import { formatFileSize, getFileIcon } from "./ai-utils";
-import type { WaveAIModel } from "./waveai-model";
-
-interface AIDroppedFilesProps {
- model: WaveAIModel;
-}
-
-export const AIDroppedFiles = memo(({ model }: AIDroppedFilesProps) => {
- const droppedFiles = useAtomValue(model.droppedFiles);
-
- if (droppedFiles.length === 0) {
- return null;
- }
-
- return (
-
-
-
- {showNoToolsWarning && (
-
- Warning: This custom mode was configured without the "tools" capability in the
- "ai:capabilities" array. Without tool support, Wave AI will not be able to interact with
- widgets or files.
-
- Wave AI is your terminal assistant with context. I can read your terminal output, analyze widgets,
- access files, and help you solve problems faster.
-
-
-
Getting Started:
-
-
-
-
-
-
- Widget Context
-
When ON, I can read your terminal and analyze widgets.
- Wave AI now supports bring-your-own-key (BYOK) with OpenAI, Google Gemini, Azure, and
- OpenRouter, plus local models via Ollama, LM Studio, and other OpenAI-compatible providers.
-
- This will restore {toolData.inputfilename}{" "}
- to its state before this edit was made
- {toolData.runts && ({formatTimestamp(toolData.runts)})}.
-
-
- Any changes made by this edit and subsequent edits will be lost.
-
- Wave AI is free to use and provides integrated AI chat that can interact with your widgets,
- help you with code, analyze files, and assist with your terminal workflows.
-
-
-
-
-
-
-
-
Telemetry keeps Wave AI free
-
-
- To keep Wave AI free for everyone, we require a small amount of anonymous{" "}
- usage data (app version, feature usage, system info).
-
-
- This helps us block abuse by automated systems and ensure it's used by real
- people like you.
-
-
- We never collect your files, prompts, keystrokes, hostnames, or personally
- identifying information. Wave AI is powered by OpenAI's APIs, please refer to
- OpenAI's privacy policy for details on how they handle your data.
-
- Wave AI is your terminal assistant with context. I can read your terminal output,
- analyze widgets, read/write files, and help you solve problems faster.
-
-
-
-
-
- Toggle the Wave AI panel with the{" "}
-
-
- AI
- {" "}
- button in the header (top left)
-
-
-
-
-
-
- Or use the keyboard shortcut{" "}
-
- {shortcutKey}
- {" "}
- to quickly toggle
-
-
-
-
-
-
- Bring your own API keys or run local models with Ollama, LM Studio, and other
- OpenAI-compatible providers
-