Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
20de8a8
add session-level auto-approve for AI file read operations
programista-wordpress Mar 21, 2026
03ae5bd
fix: handle Windows path separators in directory extraction
programista-wordpress Mar 21, 2026
0ab2969
fix: block sensitive directories from session auto-approval
programista-wordpress Mar 21, 2026
42a16e7
fix: canonicalize paths with symlink resolution to prevent bypass
programista-wordpress Mar 21, 2026
12f4861
feat: add MCP (Model Context Protocol) client package
programista-wordpress Mar 22, 2026
85eb4b0
feat: integrate MCP with AI chat pipeline
programista-wordpress Mar 22, 2026
f7f72c6
feat: MCP Context toggle and auto-detect in AI panel
programista-wordpress Mar 22, 2026
69c7e84
feat: MCP Client widget with tools panel and call log
programista-wordpress Mar 22, 2026
92c9603
feat: web content tools - read text, read HTML, SEO audit
programista-wordpress Mar 22, 2026
0e6a6bc
feat: session history - persist and display previous AI sessions
programista-wordpress Mar 22, 2026
dd7f3d8
feat: AI execution plans with progress tracking
programista-wordpress Mar 22, 2026
b4c8402
feat: project instructions reader (WAVE.md, CLAUDE.md, .cursorrules)
programista-wordpress Mar 22, 2026
b02b7f6
perf: compress tool descriptions and consolidate utility tools
programista-wordpress Mar 22, 2026
df51b85
fix: syntax highlighting in AI diff viewer
programista-wordpress Mar 22, 2026
7a0db93
feat: Quick Add Model with BYOK presets
programista-wordpress Mar 22, 2026
f6d1e4f
feat: graceful shutdown for MCP clients and session history save
programista-wordpress Mar 22, 2026
7df705f
fix: improve AI message handling and shell command detection
programista-wordpress Mar 22, 2026
54da2fc
docs: update README with MCP, web tools, plans, session history, and …
programista-wordpress Mar 22, 2026
ce1f244
feat: improve AI quality - project stack context, detailed plans, pro…
programista-wordpress Mar 22, 2026
7c39a6b
security: sanitize WebSelector opts in RPC handler
programista-wordpress Mar 22, 2026
e2b0558
fix: use error banner instead of API key input for Ollama connection …
programista-wordpress Mar 22, 2026
e189c5a
fix: only send mcpcwd when MCP context is enabled
programista-wordpress Mar 22, 2026
d1532e5
a11y: add switch role and aria-label to toggle buttons
programista-wordpress Mar 22, 2026
776abcb
fix: prevent panic on missing step_id in plan_update
programista-wordpress Mar 22, 2026
5aa0c87
fix: remove hardcoded tool names and approval language from system pr…
programista-wordpress Mar 22, 2026
e17cc55
fix: kill MCP process on read timeout to prevent goroutine leak
programista-wordpress Mar 22, 2026
4b32276
brand: introduce Wove - AI-first terminal built on Wave engine
programista-wordpress Mar 22, 2026
669c647
brand: rename to Wove - AI-first terminal
programista-wordpress Mar 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,79 @@ Wave AI is your context-aware terminal assistant with access to your workspace:
- **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
- **Quick Add Model**: Add AI providers in 3 clicks - kebab menu, pick provider, paste API key
- **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).

## MCP Integration

Wave Terminal supports the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) - giving AI full context of your project without manual configuration.

- **Auto-detect**: Wave finds `.mcp.json` in your terminal's working directory and offers to connect
- **Project Context**: AI automatically gets database schema, application info, and framework documentation
- **AI Tools**: MCP tools are registered as AI tools - the model queries your database, searches docs, and reads logs on its own
- **MCP Client Widget**: Dedicated widget showing server status, available tools, and a live call log with expandable results
- **Any MCP Server**: Works with Laravel Boost, Prisma, Django, or any MCP-compatible server

Add a `.mcp.json` to your project root:
```json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["mcp-server.js"]
}
}
}
```

## Web Content Tools

AI can read and analyze web pages directly from Wave's web widget:

- **web_read_text**: Extract clean text from pages by CSS selector
- **web_read_html**: Get raw HTML for structure inspection
- **web_seo_audit**: Full SEO audit - JSON-LD, Open Graph, meta tags, headings, alt text, link statistics
- **AI Reading Animation**: Visual highlight on elements being read by AI
- Pages auto-refresh before reading to ensure fresh content

## Execution Plans

For complex multi-step tasks, AI creates execution plans with progress tracking:

- **Plan Creation**: AI breaks tasks into steps (e.g., audit 10 pages, process multiple files)
- **Step-by-step Execution**: Each step runs independently with clean context
- **Live Progress Panel**: Visual progress bar and expandable step results in the AI panel
- **Persistent**: Plans survive Wave restarts, AI continues from where it left off
- **Dismiss**: Close completed plans with one click

## Session History

AI remembers what you did in previous sessions:

- **Auto-save**: Chat history saved per tab when Wave shuts down
- **Previous Session Banner**: Expandable summary of last session's messages and tool calls
- **session_history Tool**: AI reads previous work context on demand
- **Per-tab**: Each tab maintains its own history independently

## Project Instructions

Wave reads project-specific coding instructions from `WAVE.md`, `CLAUDE.md`, `.cursorrules`, and other convention files:

- **Smart Filtering**: AI requests only relevant sections (e.g., PHP sections when editing .php files)
- **Table of Contents**: First call lists available sections, second call fetches specific ones
- **Multiple Files**: Reads all instruction files found and combines them
- **Token Efficient**: Two-step approach minimizes context usage for smaller models

## Auto-approve for File Reading

AI can read files without asking for approval each time:

- **Session-level Approval**: Approve a directory once, all reads within it are auto-approved
- **Sensitive Path Protection**: ~/.ssh, ~/.aws, .env files are never auto-approved
- **Symlink Safety**: Canonical path resolution prevents bypass via symlinks

## Installation

Wave Terminal works on macOS, Linux, and Windows.
Expand Down
4 changes: 4 additions & 0 deletions cmd/server/main-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import (
"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/aiusechat/sessionhistory"
"github.com/wavetermdev/waveterm/pkg/mcpclient"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wcloud"
Expand Down Expand Up @@ -81,6 +83,8 @@ func doShutdown(reason string) {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
go blockcontroller.StopAllBlockControllersForShutdown()
sessionhistory.SaveAll()
mcpclient.GetManager().Shutdown()
shutdownActivityUpdate()
sendTelemetryWrapper()
// TODO deal with flush in progress
Expand Down
98 changes: 96 additions & 2 deletions emain/emain-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,115 @@ function escapeSelector(selector: string): string {
export type WebGetOpts = {
all?: boolean;
inner?: boolean;
innertext?: boolean;
reload?: boolean;
execjs?: string;
highlight?: boolean;
};

export async function webGetSelector(wc: WebContents, selector: string, opts?: WebGetOpts): Promise<string[]> {
if (!wc || !selector) {
return null;
}

// Reload the page if requested, then wait for it to finish loading
if (opts?.reload) {
wc.reload();
await new Promise<void>((resolve) => {
const onFinish = () => {
wc.removeListener("did-finish-load", onFinish);
resolve();
};
wc.on("did-finish-load", onFinish);
// Timeout fallback in case did-finish-load doesn't fire
setTimeout(() => {
wc.removeListener("did-finish-load", onFinish);
resolve();
}, 10000);
});
}

// Custom JS execution mode — run arbitrary JS and return result as string array
if (opts?.execjs) {
const customExpr = `
(async () => {
try {
const result = await (async () => { ${opts.execjs} })();
if (Array.isArray(result)) {
return { value: result.map(String) };
}
return { value: [String(result)] };
} catch (error) {
return { error: error.message };
}
})()`;
const results = await wc.executeJavaScript(customExpr);
if (results.error) {
throw new Error(results.error);
}
return results.value;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const escapedSelector = escapeSelector(selector);
const queryMethod = opts?.all ? "querySelectorAll" : "querySelector";
const prop = opts?.inner ? "innerHTML" : "outerHTML";
const prop = opts?.innertext ? "innerText" : opts?.inner ? "innerHTML" : "outerHTML";
const doHighlight = opts?.highlight ?? false;
const execExpr = `
(() => {
const toArr = x => (x instanceof NodeList) ? Array.from(x) : (x ? [x] : []);
try {
const result = document.${queryMethod}("${escapedSelector}");
const value = toArr(result).map(el => el.${prop});
const els = toArr(result);
const value = els.map(el => el.${prop});

if (${doHighlight} && els.length > 0) {
// Inject highlight styles once
if (!document.getElementById('__wave_ai_highlight_style')) {
const style = document.createElement('style');
style.id = '__wave_ai_highlight_style';
style.textContent = \`
@keyframes __wave_ai_scan {
0% { box-shadow: 0 0 0 2px rgba(99, 102, 241, 0); border-color: rgba(99, 102, 241, 0); }
15% { box-shadow: 0 0 8px 2px rgba(99, 102, 241, 0.4); border-color: rgba(99, 102, 241, 0.8); }
100% { box-shadow: 0 0 0 2px rgba(99, 102, 241, 0); border-color: rgba(99, 102, 241, 0); }
}
.__wave_ai_reading {
outline: 2px solid rgba(99, 102, 241, 0.7) !important;
outline-offset: 2px !important;
animation: __wave_ai_scan 2s ease-out forwards !important;
position: relative !important;
}
.__wave_ai_reading::after {
content: 'AI Reading...' !important;
position: absolute !important;
top: -22px !important;
right: 0 !important;
background: rgba(99, 102, 241, 0.9) !important;
color: white !important;
font-size: 10px !important;
padding: 2px 8px !important;
border-radius: 4px !important;
font-family: system-ui, sans-serif !important;
z-index: 999999 !important;
pointer-events: none !important;
animation: __wave_ai_scan 2s ease-out forwards !important;
}
\`;
document.head.appendChild(style);
}

// Apply highlight to matched elements
els.forEach(el => {
el.classList.add('__wave_ai_reading');
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});

// Remove highlight after animation
setTimeout(() => {
els.forEach(el => el.classList.remove('__wave_ai_reading'));
}, 2500);
}

return { value };
} catch (error) {
return { error: error.message };
Expand Down
21 changes: 18 additions & 3 deletions frontend/app/aipanel/ai-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,8 @@ export const getFilteredAIModeConfigs = (
showCloudModes: boolean,
inBuilder: boolean,
hasPremium: boolean,
currentMode?: string
currentMode?: string,
availableSecrets?: Set<string>
): FilteredAIModeConfigs => {
const hideQuick = inBuilder && hasPremium;

Expand All @@ -557,10 +558,24 @@ export const getFilteredAIModeConfigs = (
.filter((config) => !(hideQuick && config.mode === "waveai@quick"));

const otherProviderConfigs = allConfigs
.filter((config) => config["ai:provider"] !== "wave")
.filter((config) => {
if (config["ai:provider"] === "wave") return false;
// Hide byok presets that need API key unless the secret exists
if (config.mode.startsWith("byok@")) {
const secretName = config["ai:apitokensecretname"];
if (secretName) {
return config["ai:apitoken"] || (availableSecrets && availableSecrets.has(secretName));
}
// No secret needed (local models) - show only if marker secret exists
// Set by Quick Add Model after verifying the endpoint works
return availableSecrets && availableSecrets.has("byok-local-enabled");
}
return true;
})
.sort(sortByDisplayOrder);

const hasCustomModels = otherProviderConfigs.length > 0;
// Only count user-configured custom models, not built-in byok presets
const hasCustomModels = otherProviderConfigs.some((config) => !config.mode.startsWith("byok@"));
const isCurrentModeCloud = currentMode?.startsWith("waveai@") ?? false;
const shouldShowCloudModes = showCloudModes || !hasCustomModels || isCurrentModeCloud;

Expand Down
33 changes: 31 additions & 2 deletions frontend/app/aipanel/aimessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

import { WaveStreamdown } from "@/app/element/streamdown";
import { cn } from "@/util/util";
import { memo, useEffect, useRef } from "react";
import { atoms, globalStore } from "@/app/store/global";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import * as WOS from "@/app/store/wos";
import { cn, stringToBase64 } from "@/util/util";
import { memo, useCallback, useEffect, useRef } from "react";
import { getFileIcon } from "./ai-utils";
import { AIFeedbackButtons } from "./aifeedbackbuttons";
import { AIToolUseGroup } from "./aitooluse";
Expand Down Expand Up @@ -110,9 +114,33 @@ interface AIMessagePartProps {
isStreaming: boolean;
}

function findFirstTerminalBlockId(): string | null {
const tabId = globalStore.get(atoms.staticTabId);
const tabObj = WOS.getObjectValue<Tab>(WOS.makeORef("tab", tabId));
if (!tabObj?.blockids) return null;
for (const blockId of tabObj.blockids) {
const block = WOS.getObjectValue<Block>(WOS.makeORef("block", blockId));
if (block?.meta?.view === "term") {
return blockId;
}
}
return null;
}

function sendCommandToTerminal(cmd: string) {
const blockId = findFirstTerminalBlockId();
if (!blockId) return;
const b64data = stringToBase64(cmd + "\n");
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: blockId, inputdata64: b64data });
}

const AIMessagePart = memo(({ part, role, isStreaming }: AIMessagePartProps) => {
const model = WaveAIModel.getInstance();

const handleExecute = useCallback((cmd: string) => {
sendCommandToTerminal(cmd);
}, []);

if (part.type === "text") {
const content = part.text ?? "";

Expand All @@ -125,6 +153,7 @@ const AIMessagePart = memo(({ part, role, isStreaming }: AIMessagePartProps) =>
parseIncompleteMarkdown={isStreaming}
className="text-gray-100"
codeBlockMaxWidthAtom={model.codeBlockMaxWidth}
onClickExecute={handleExecute}
/>
);
}
Expand Down
13 changes: 11 additions & 2 deletions frontend/app/aipanel/aimode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { cn, fireAndForget, makeIconClass } from "@/util/util";
import { useAtomValue } from "jotai";
import { memo, useRef, useState } from "react";
import { memo, useEffect, useRef, useState } from "react";
import { getFilteredAIModeConfigs, getModeDisplayName } from "./ai-utils";
import { WaveAIModel } from "./waveai-model";

Expand Down Expand Up @@ -146,14 +146,23 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow
const showCloudModes = useAtomValue(getSettingsKeyAtom("waveai:showcloudmodes"));
const telemetryEnabled = useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false;
const [isOpen, setIsOpen] = useState(false);
const [availableSecrets, setAvailableSecrets] = useState<Set<string>>(new Set());
const dropdownRef = useRef<HTMLDivElement>(null);

// Load available secret names to filter byok presets
useEffect(() => {
RpcApi.GetSecretsNamesCommand(TabRpcClient)
.then((names) => setAvailableSecrets(new Set(names || [])))
.catch(() => {});
}, [isOpen]);

const { waveProviderConfigs, otherProviderConfigs } = getFilteredAIModeConfigs(
aiModeConfigs,
showCloudModes,
model.inBuilder,
hasPremium,
currentMode
currentMode,
availableSecrets
);

const sections: ConfigSection[] = compatibilityMode
Expand Down
Loading