Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
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 on lines +69 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 '\b(execjs|ExecJs)\b' emain pkg frontend
rg -n -C3 '\bwebGetSelector\s*\(' emain pkg frontend

Repository: wavetermdev/waveterm

Length of output: 2483


🏁 Script executed:

# Find the definition of seoAuditJS and the webReadContent function
rg -n "seoAuditJS" emain pkg frontend
rg -n "webReadContent" emain pkg frontend

Repository: wavetermdev/waveterm

Length of output: 885


🏁 Script executed:

# Check the RPC handler and how data flows from client to webGetSelector
rg -n -A10 "webGetSelector" emain/emain-wsh.ts

Repository: wavetermdev/waveterm

Length of output: 1029


🏁 Script executed:

# Find where WebSelectorOpts/WebGetOpts are constructed or passed
rg -n "WebSelectorOpts\|WebGetOpts" emain pkg frontend --type go --type ts

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Find CommandWebSelectorData definition
rg -n "CommandWebSelectorData\|type.*WebSelector" pkg frontend --type ts --type go

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Check where the webselector RPC endpoint is invoked from
rg -n "webselector\|CommandWebSelector" pkg frontend --type ts --type go -i

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Verify seoAuditJS content to understand scope
sed -n '255,300p' pkg/aiusechat/tools_web.go

Repository: wavetermdev/waveterm

Length of output: 1531


🏁 Script executed:

# Search for CommandWebSelectorData and WebSelector type definitions
fd -e go -e ts | xargs grep -l "CommandWebSelector\|WebSelectorData" | head -20

Repository: wavetermdev/waveterm

Length of output: 258


🏁 Script executed:

# Look at wshrpc protocol definitions
find pkg/wshrpc -type f -name "*.go" | xargs grep -n "WebSelector\|webselector"

Repository: wavetermdev/waveterm

Length of output: 764


🏁 Script executed:

# Check if there's a protocol definition file that defines the command structure
fd -e proto -o -e ts -o -e go | xargs grep -l "webselector" 2>/dev/null | head -10

Repository: wavetermdev/waveterm

Length of output: 180


🏁 Script executed:

# Check wshclientapi.ts for WebSelectorCommand usage
rg -n -A5 "WebSelectorCommand" frontend/app/store/wshclientapi.ts

Repository: wavetermdev/waveterm

Length of output: 411


🏁 Script executed:

# Look at the full CommandWebSelectorData definition
sed -n '482,490p' pkg/wshrpc/wshrpctypes.go

Repository: wavetermdev/waveterm

Length of output: 376


🏁 Script executed:

# Search for who calls webselector or WebSelectorCommand from frontend
rg -n "webselector\|WebSelectorCommand" frontend --type ts

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Find all calls to webGetSelector
rg -n "webGetSelector" emain frontend pkg --type ts --type go

Repository: wavetermdev/waveterm

Length of output: 384


🏁 Script executed:

# Check if webSelectorInput is parsed from user input
rg -n -B5 -A10 "webSelectorInput" pkg/aiusechat/tools_web.go

Repository: wavetermdev/waveterm

Length of output: 1424


🏁 Script executed:

# Search for tool definitions exposed to AI models
rg -n "func.*Tool\|registerTool\|AddTool" pkg/aiusechat/tools_web.go -A3

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Find the tool definitions and how they're registered
rg -n "GetWebSelectorTools\|func.*Web.*Tool" pkg/aiusechat/tools_web.go -B2 -A15

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Check how tools are used/exposed to AI models
rg -n "Tool\|Execute\|Function" pkg/aiusechat/ --type go | grep -i "web\|tool" | head -30

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Look at the beginning of tools_web.go to understand the structure
head -150 pkg/aiusechat/tools_web.go

Repository: wavetermdev/waveterm

Length of output: 4305


🏁 Script executed:

# Find the tool function that invokes webReadContent with hardcoded opts
rg -n -B10 "GetWebSelectorToolDefinition\|GetWebReadContentTool" pkg/aiusechat/tools_web.go | head -100

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Check if there's a registry of available tools and how they're used
rg -n "RegisterTool\|AvailableTools\|GetTool" pkg/aiusechat/ --type go | head -20

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Look for the RPC handler registration and validation
rg -n "handle_webselector" emain pkg --type ts --type go -A5

Repository: wavetermdev/waveterm

Length of output: 527


🏁 Script executed:

# Search more carefully for tool definitions - look for GetWeb functions
rg -n "^func GetWeb" pkg/aiusechat/tools_web.go

Repository: wavetermdev/waveterm

Length of output: 370


🏁 Script executed:

# Check if there's any validation or filtering of opts in the RPC handler
rg -n -A20 "handle_webselector" emain/emain-wsh.ts

Repository: wavetermdev/waveterm

Length of output: 1108


🏁 Script executed:

# Search for how tools are exposed - look for tool lists or registrations
rg -n "GetAvailableTools\|tool.*list\|registerTools" pkg/aiusechat --type go

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Check how the AI models use tools - are they exposed via tool definitions or direct RPC?
rg -n "ToolDefinition\|ToolAnyCallback" pkg/aiusechat/tools_web.go | head -30

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Look at the GetWebSEOAuditToolDefinition to see how it calls webReadContent
sed -n '329,400p' pkg/aiusechat/tools_web.go

Repository: wavetermdev/waveterm

Length of output: 1227


🏁 Script executed:

# Check if there's any RPC request validation or if all RPC endpoints are freely callable
rg -n "RpcResponseHelper\|validateInput\|checkAccess" emain/emain-wsh.ts | head -20

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Check if RPC endpoints are validated/filtered or if all can be called freely
rg -n "wshRpcCall\|rpc.*webselector" frontend --type ts -i

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Look for the RPC server implementation to see if there's a whitelist of allowed commands
rg -n "RegisterCommand\|CommandHandler\|CommandMap" pkg/wshrpc --type go

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Check if there's any sanitization or validation of opts before it reaches the handler
rg -n -B5 "data.opts" emain/emain-wsh.ts

Repository: wavetermdev/waveterm

Length of output: 361


🏁 Script executed:

# Check if there's authentication/authorization for RPC endpoints
rg -n "authenticat\|authoriz\|permission" pkg/wshrpc emain --type go --type ts | head -20

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Look for how the RPC server is set up and if there's endpoint filtering
rg -n "NewWshServer\|RegisterCommand\|handleRequest" pkg/wshrpc --type go | head -20

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Final check: Look at how frontend constructs and sends RPC calls - is there any input validation?
rg -n "wshRpcCall" frontend/app --type ts -A2 | head -30

Repository: wavetermdev/waveterm

Length of output: 1609


🏁 Script executed:

# Final verification: check if WebSelectorCommand is ever called from frontend, especially with user input
rg -n "WebSelectorCommand" frontend --type ts

Repository: wavetermdev/waveterm

Length of output: 212


🏁 Script executed:

# Check the exact signature and usage of WebSelectorCommand one more time
rg -n -B2 -A8 "WebSelectorCommand.*function\|WebSelectorCommand.*=>.*{" frontend/app/store/wshclientapi.ts

Repository: wavetermdev/waveterm

Length of output: 237


🏁 Script executed:

# Search for all calls to WebSelectorCommand in the entire codebase
rg -n "WebSelectorCommand\(" emain pkg frontend --type ts --type go

Repository: wavetermdev/waveterm

Length of output: 709


🏁 Script executed:

# Check if there are any other places where the webselector RPC is invoked
rg -n "webselector" frontend --type ts

Repository: wavetermdev/waveterm

Length of output: 383


🏁 Script executed:

# Look at the exact line where WebSelectorCommand is called in tools_web.go to see what opts are passed
sed -n '150,180p' pkg/aiusechat/tools_web.go

Repository: wavetermdev/waveterm

Length of output: 958


🏁 Script executed:

# Check if there's any validation in the RPC handler for opts parameter
rg -n -A30 "handle_webselector" emain/emain-wsh.ts

Repository: wavetermdev/waveterm

Length of output: 1491


🏁 Script executed:

# Search for the RPC server implementation - how are commands routed and validated
fd -e go -path "*/wshrpc/*" -o -path "*/wshutil/*" | xargs grep -l "dispatch\|route\|handle.*command" | head -5

Repository: wavetermdev/waveterm

Length of output: 232


🏁 Script executed:

# Check if there's any global RPC validation or filtering mechanism
rg -n "validateCommand\|authorizeCommand\|checkPermission" pkg/wshrpc pkg/wshutil --type go

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Verify: is WebSelectorCommand EVER called from frontend besides in wshclientapi definition?
rg -n "\.WebSelectorCommand\|wshRpcCall.*webselector" frontend --type ts -i

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Look at how RPC messages are routed - check wshutil or wshrpc server code
find pkg/wshrpc pkg/wshutil -name "*.go" | xargs grep -l "Route\|dispatch\|command" | head -3

Repository: wavetermdev/waveterm

Length of output: 173


🏁 Script executed:

# Check the RPC server code for how commands are dispatched
rg -n "func.*Route\|func.*Dispatch\|command.*=>" pkg/wshrpc --type go | head -20

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Final verification: Can frontend construct arbitrary RPC calls or are they limited to wshclientapi methods?
rg -n "wshRpcCall\|wshclient" frontend/app/store --type ts | grep -v "wshclientapi\|wshclient.ts" | head -10

Repository: wavetermdev/waveterm

Length of output: 46


Add input validation for opts parameter in RPC handler to prevent arbitrary JavaScript execution.

At lines 69–86, this code executes arbitrary JavaScript if opts.execjs is populated. While current AI tool code paths hardcode safe values (only seoAuditJS is used for SEO auditing), the RPC handler at emain-wsh.ts:30 accepts and forwards data.opts directly without validation. Future code changes, frontend modifications, or misuse could introduce user/model-controlled input into execjs.

Add validation in handle_webselector to either:

  • Reject/filter unsupported fields in opts (block execjs entirely from RPC)
  • Only allow safe option combinations (whitelist-based validation)

This protects against accidental exposure and ensures the RPC boundary enforces intent, not just current code patterns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@emain/emain-web.ts` around lines 69 - 87, The RPC handler handle_webselector
must validate the incoming opts argument to prevent arbitrary execution via
opts.execjs; update handle_webselector to either reject any opts containing the
execjs property or implement a strict whitelist of allowed option keys (e.g.,
only allow seoAuditJS or other explicit safe fields) and return an error when
unknown/blocked keys are present, ensuring that the opts object forwarded to the
emain-web path never contains execjs or other executable payloads; locate
references to opts and execjs in handle_webselector and the emain-web invocation
flow and enforce this validation before forwarding.


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