Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
133 changes: 78 additions & 55 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,82 @@ Want input on the roadmap? Join the discussion on [Discord](https://discord.gg/X

Legend: ✅ Done | 🔧 In Progress | 🔷 Planned | 🤞 Stretch Goal

## v0.11.0

Released on 1/25/25

- ✅ File/Directory Preview improvements
- ✅ Reworked fileshare layer running over RPC
- ✅ Expanded URI types supported by `wsh file ...`
- ✅ EC-TIME timeout when transferring large files
- ✅ Fixes for reducing 2FA requests on connect
- ✅ WebLinks in the terminal working again
- ✅ Search in Web Views
- ✅ Search in the Terminal
- ✅ Custom init files for widgets and terminal blocks
- ✅ Multi-Input between terminal blocks on the same tab
- ✅ Gemini AI support
- ✅ Various Connection Bugs + Improvements
- ✅ More Connection Config Options

## v0.11.1

Targeting 1/31/25

- 🔧 Reduce main-line 2FA requests to 1 per connection
- 🔧 Remote S3 bucket browsing (directory + files)
- 🔷 Drag & drop between preview blocks
- 🔷 Drag into/out of a preview block from native file explorer
- 🔷 Wave Apps (Go SDK)
- 🔷 JSON schema support (basic)
- 🤞 Frontend Only Widgets, React + Babel Transpiling in an iframe/webview

## v0.12

Targeting mid-February.

- 🔷 Import/Export Tab Layouts and Widgets
- 🔷 log viewer
- 🔷 binary viewer
- 🔷 New layout actions (splitting, replacing blocks)
- 🔷 Rewrite of window/tab system
- 🔷 Minimized / Non-Visible blocks
- 🔷 Custom keybindings to quickly switch / invoke built-in and custom widgets
- 🔷 More Drag & Drop support of files/URLs to create blocks
- 🔷 Tab Templates

## Planned (Unscheduled)

- 🔷 Customizable Keybindings
- 🔷 Launch widgets with custom keybindings
- 🔷 Re-assign system keybindings
## Current AI Capabilities

Wave Terminal's AI assistant is already powerful and continues to evolve. Here's what works today:

### AI Provider Support

- ✅ OpenAI (including gpt-5 and gpt-5-mini models)

### Context & Input

- ✅ Widget context integration - AI sees your open terminals, web views, and other widgets
- ✅ Image and document upload - Attach images and files to conversations
- ✅ Local file reading - Read text files and directory listings on local machine
- ✅ Web search - Native web search capability for current information
- ✅ Shell integration awareness - AI understands terminal state (shell, version, OS, etc.)

### Widget Interaction Tools

- ✅ Widget screenshots - Capture visual state of any widget
- ✅ Terminal scrollback access - Read terminal history and output
- ✅ Web navigation - Control browser widgets

## ROADMAP Enhanced AI Capabilities

### AI Configuration & Flexibility

- 🔷 BYOK (Bring Your Own Key) - Use your own API keys for any supported provider
- 🔧 Enhanced provider configuration options
- 🔷 Context (add markdown files to give persistent system context)

### Expanded Provider Support

Top priorities are Claude (for better coding support), and the OpenAI Completions API which will allow us to interface with
many more local/open models.

- 🔷 Anthropic Claude - Full integration with extended thinking and tool use
- 🔷 OpenAI Completions API - Support for older model formats
- 🤞 Google Gemini - Complete integration
- 🤞 Local AI agents - Run AI models locally on your machine

### Advanced AI Tools

#### File Operations

- 🔧 AI file writing with intelligent diff previews
- 🔧 Rollback support for AI-made changes
- 🔷 Multi-file editing workflows
- 🔷 Safe file modification patterns

#### Terminal Command Execution

- 🔧 Execute commands directly from AI
- 🔧 Intelligent terminal state detection
- 🔧 Command result capture and parsing

### Remote & Advanced Capabilities

- 🔷 Remote file operations - Read and write files on SSH connections
- 🔷 Custom AI-powered widgets (Tsunami framework)
- 🔷 AI Can spawn Wave Blocks
- 🔷 Drag&Drop from Preview Widgets to Wave AI

### Wave AI Widget Builder

- 🔷 Visual builder for creating custom AI-powered widgets
- 🔷 Template library for common AI workflows
- 🔷 Rapid prototyping and iteration tools

## Other Platform & UX Improvements (Non AI)

- 🔷 Import/Export tab layouts and widgets
- 🔧 Enhanced layout actions (splitting, replacing blocks)
- 🔷 Extended drag & drop for files/URLs
- 🔷 Tab templates for quick workspace setup
- 🔷 Advanced keybinding customization
- 🔷 Widget launch shortcuts
- 🔷 System keybinding reassignment
- 🔷 Command Palette
- 🔷 AI Context
- 🔷 Monaco Theming
- 🔷 File system watching for Preview
- 🔷 File system watching for drag and drop
- 🤞 Explore VSCode Extension Compatibility with standalone Monaco Editor (language servers)
- 🤞 VSCode File Icons in Preview
- 🔷 Monaco Editor theming
21 changes: 21 additions & 0 deletions cmd/server/main-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/authkey"
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
"github.com/wavetermdev/waveterm/pkg/filebackup"
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
Expand Down Expand Up @@ -51,6 +52,8 @@ 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

var shutdownOnce sync.Once

Expand Down Expand Up @@ -114,6 +117,23 @@ func telemetryLoop() {
}
}

func backupCleanupLoop() {
defer func() {
panichandler.PanicHandler("backupCleanupLoop", recover())
}()
var nextCleanup int64
for {
if time.Now().Unix() > nextCleanup {
nextCleanup = time.Now().Add(BackupCleanupInterval).Unix()
err := filebackup.CleanupOldBackups()
if err != nil {
log.Printf("error cleaning up old backups: %v\n", err)
}
}
time.Sleep(BackupCleanupTick)
}
}

func panicTelemetryHandler(panicName string) {
activity := wshrpc.ActivityUpdate{NumPanics: 1}
err := telemetry.UpdateActivity(context.Background(), activity)
Expand Down Expand Up @@ -413,6 +433,7 @@ func main() {
go stdinReadWatch()
go telemetryLoop()
go updateTelemetryCountsLoop()
go backupCleanupLoop()
go startupActivityUpdate(firstLaunch) // must be after startConfigWatcher()
blocklogger.InitBlockLogger()
go wavebase.GetSystemSummary() // get this cached (used in AI)
Expand Down
1 change: 1 addition & 0 deletions docs/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ wsh editconfig
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
| editor:wordwrap | bool | set to true to enable word wrapping in the editor (defaults to false) |
| editor:fontsize | float64 | set the font size for the editor (defaults to 12px) |
| editor:inlinediff | bool | set to true to show diffs inline instead of side-by-side, false for side-by-side (defaults to undefined which uses Monaco's responsive behavior) |
| preview:showhiddenfiles | bool | set to false to disable showing hidden files in the directory preview (defaults to true) |
| markdown:fontsize | float64 | font size for the normal text when rendering markdown in preview. headers are scaled up from this size, (default 14px) |
| markdown:fixedfontsize | float64 | font size for the code blocks when rendering markdown in preview (default is 12px) |
Expand Down
69 changes: 44 additions & 25 deletions frontend/app/aipanel/aitooluse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { BlockModel } from "@/app/block/block-model";
import { cn } from "@/util/util";
import { cn, fireAndForget } from "@/util/util";
import { memo, useEffect, useRef, useState } from "react";
import { WaveUIMessagePart } from "./aitypes";
import { WaveAIModel } from "./waveai-model";
Expand Down Expand Up @@ -141,6 +141,8 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => {
const baseApproval = userApprovalOverride || toolData.approval;
const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval;

const isFileWriteTool = toolData.toolname === "write_text_file" || toolData.toolname === "edit_text_file";

useEffect(() => {
if (!isStreaming || effectiveApproval !== "needs-approval") return;

Expand Down Expand Up @@ -204,6 +206,10 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => {
}
};

const handleOpenDiff = () => {
fireAndForget(() => WaveAIModel.getInstance().openDiff(toolData.inputfilename, toolData.toolcallid));
};

return (
<div
className={cn("flex items-start gap-2 p-2 rounded bg-gray-800 border border-gray-700", statusColor)}
Expand All @@ -221,6 +227,16 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => {
<AIToolApprovalButtons count={1} onApprove={handleApprove} onDeny={handleDeny} />
)}
</div>
{isFileWriteTool && toolData.inputfilename && (
<button
onClick={handleOpenDiff}
className="flex-shrink-0 px-2 py-1 border border-gray-600 hover:border-gray-500 hover:bg-gray-700 rounded cursor-pointer transition-colors flex items-center gap-1.5 text-gray-400"
title="Open in diff viewer"
>
<span className="text-sm">Show Diff</span>
<i className="fa fa-arrow-up-right-from-square text-sm"></i>
</button>
)}
</div>
);
});
Expand All @@ -232,47 +248,50 @@ interface AIToolUseGroupProps {
isStreaming: boolean;
}

type ToolGroupItem =
| { type: "batch"; parts: Array<WaveUIMessagePart & { type: "data-tooluse" }> }
| { type: "single"; part: WaveUIMessagePart & { type: "data-tooluse" } };

export const AIToolUseGroup = memo(({ parts, isStreaming }: AIToolUseGroupProps) => {
const isFileOp = (part: WaveUIMessagePart & { type: "data-tooluse" }) => {
const toolName = part.data?.toolname;
return toolName === "read_text_file" || toolName === "read_dir";
};

const fileOpsNeedApproval: Array<WaveUIMessagePart & { type: "data-tooluse" }> = [];
const fileOpsNoApproval: Array<WaveUIMessagePart & { type: "data-tooluse" }> = [];
const otherTools: Array<WaveUIMessagePart & { type: "data-tooluse" }> = [];
const groupedItems: ToolGroupItem[] = [];
let currentBatch: Array<WaveUIMessagePart & { type: "data-tooluse" }> = [];

for (const part of parts) {
if (isFileOp(part)) {
if (part.data.approval === "needs-approval") {
fileOpsNeedApproval.push(part);
} else {
fileOpsNoApproval.push(part);
}
currentBatch.push(part);
} else {
otherTools.push(part);
if (currentBatch.length > 0) {
groupedItems.push({ type: "batch", parts: currentBatch });
currentBatch = [];
}
groupedItems.push({ type: "single", part });
}
}

if (currentBatch.length > 0) {
groupedItems.push({ type: "batch", parts: currentBatch });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<>
{fileOpsNoApproval.length > 0 && (
<div className="mt-2">
<AIToolUseBatch parts={fileOpsNoApproval} isStreaming={isStreaming} />
</div>
{groupedItems.map((item, idx) =>
item.type === "batch" ? (
<div key={idx} className="mt-2">
<AIToolUseBatch parts={item.parts} isStreaming={isStreaming} />
</div>
) : (
<div key={idx} className="mt-2">
<AIToolUse part={item.part} isStreaming={isStreaming} />
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
)
)}
{fileOpsNeedApproval.length > 0 && (
<div className="mt-2">
<AIToolUseBatch parts={fileOpsNeedApproval} isStreaming={isStreaming} />
</div>
)}
{otherTools.map((tool, idx) => (
<div key={idx} className="mt-2">
<AIToolUse part={tool} isStreaming={isStreaming} />
</div>
))}
</>
);
});

AIToolUseGroup.displayName = "AIToolUseGroup";
AIToolUseGroup.displayName = "AIToolUseGroup";
3 changes: 3 additions & 0 deletions frontend/app/aipanel/aitypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import { ChatRequestOptions, FileUIPart, UIMessage, UIMessagePart } from "ai";

type WaveUIDataTypes = {
// pkg/aiusechat/uctypes/usechat-types.go UIMessageDataUserFile
userfile: {
filename: string;
size: number;
mimetype: string;
previewurl?: string;
};
// pkg/aiusechat/uctypes/usechat-types.go UIMessageDataToolUse
tooluse: {
toolcallid: string;
toolname: string;
Expand All @@ -18,6 +20,7 @@ type WaveUIDataTypes = {
errormessage?: string;
approval?: "needs-approval" | "user-approved" | "user-denied" | "auto-approved" | "timeout";
blockid?: string;
inputfilename?: string;
};
};

Expand Down
25 changes: 24 additions & 1 deletion frontend/app/aipanel/waveai-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
WaveUIMessagePart,
} from "@/app/aipanel/aitypes";
import { FocusManager } from "@/app/store/focusManager";
import { atoms, getOrefMetaKeyAtom } from "@/app/store/global";
import { atoms, createBlock, getOrefMetaKeyAtom } from "@/app/store/global";
import { globalStore } from "@/app/store/jotaiStore";
import * as WOS from "@/app/store/wos";
import { RpcApi } from "@/app/store/wshclientapi";
Expand Down Expand Up @@ -412,6 +412,10 @@ export class WaveAIModel {
}
}

getChatId(): string {
return globalStore.get(this.chatId);
}

toolUseKeepalive(toolcallid: string) {
RpcApi.WaveAIToolApproveCommand(
TabRpcClient,
Expand All @@ -429,4 +433,23 @@ export class WaveAIModel {
approval: approval,
});
}

async openDiff(fileName: string, toolcallid: string) {
const chatId = this.getChatId();

if (!chatId || !fileName) {
console.error("Missing chatId or fileName for opening diff", chatId, fileName);
return;
}

const blockDef: BlockDef = {
meta: {
view: "aifilediff",
file: fileName,
"aifilediff:chatid": chatId,
"aifilediff:toolcallid": toolcallid,
},
};
await createBlock(blockDef, false, true);
}
Comment on lines +440 to +457
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the toolcallid parameter.

The validation at line 441 checks chatId and fileName but omits toolcallid, which is a required parameter and subsequently used in the block metadata. If toolcallid is empty or undefined, the diff view may not function correctly.

Apply this diff to validate all required parameters:

-        if (!chatId || !fileName) {
-            console.error("Missing chatId or fileName for opening diff", chatId, fileName);
+        if (!chatId || !fileName || !toolcallid) {
+            console.error("Missing required parameters for opening diff", { chatId, fileName, toolcallid });
             return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async openDiff(fileName: string, toolcallid: string) {
const chatId = this.getChatId();
if (!chatId || !fileName) {
console.error("Missing chatId or fileName for opening diff", chatId, fileName);
return;
}
const blockDef: BlockDef = {
meta: {
view: "aifilediff",
file: fileName,
"aifilediff:chatid": chatId,
"aifilediff:toolcallid": toolcallid,
},
};
await createBlock(blockDef, false, true);
}
async openDiff(fileName: string, toolcallid: string) {
const chatId = this.getChatId();
if (!chatId || !fileName || !toolcallid) {
console.error("Missing required parameters for opening diff", { chatId, fileName, toolcallid });
return;
}
const blockDef: BlockDef = {
meta: {
view: "aifilediff",
file: fileName,
"aifilediff:chatid": chatId,
"aifilediff:toolcallid": toolcallid,
},
};
await createBlock(blockDef, false, true);
}
🤖 Prompt for AI Agents
In frontend/app/aipanel/waveai-model.tsx around lines 438 to 455, the openDiff
function validates chatId and fileName but omits validating the required
toolcallid parameter; add a check that toolcallid is present (non-empty/defined)
alongside chatId and fileName, log an error including the missing values if
validation fails, and return early to avoid creating a block with incomplete
metadata.

}
2 changes: 2 additions & 0 deletions frontend/app/block/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
FullSubBlockProps,
SubBlockProps,
} from "@/app/block/blocktypes";
import { AiFileDiffViewModel } from "@/app/view/aifilediff/aifilediff";
import { LauncherViewModel } from "@/app/view/launcher/launcher";
import { PreviewModel } from "@/app/view/preview/preview-model";
import { SysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
Expand Down Expand Up @@ -50,6 +51,7 @@ BlockRegistry.set("tips", QuickTipsViewModel);
BlockRegistry.set("help", HelpViewModel);
BlockRegistry.set("launcher", LauncherViewModel);
BlockRegistry.set("tsunami", TsunamiViewModel);
BlockRegistry.set("aifilediff", AiFileDiffViewModel);

function makeViewModel(blockId: string, blockView: string, nodeModel: BlockNodeModel): ViewModel {
const ctor = BlockRegistry.get(blockView);
Expand Down
Loading
Loading