Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
96 changes: 68 additions & 28 deletions frontend/app/aipanel/aitooluse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
// 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";

function getEffectiveApprovalStatus(baseApproval: string, isStreaming: boolean): string {
return !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval;
}

interface AIToolApprovalButtonsProps {
count: number;
onApprove: () => void;
Expand Down Expand Up @@ -73,10 +77,10 @@ interface AIToolUseBatchProps {
const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => {
const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);

// All parts in a batch have the same approval status (enforced by grouping logic in AIToolUseGroup)
const firstTool = parts[0].data;
const baseApproval = userApprovalOverride || firstTool.approval;
const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval;
const allNeedApproval = parts.every((p) => (userApprovalOverride || p.data.approval) === "needs-approval");
const effectiveApproval = getEffectiveApprovalStatus(baseApproval, isStreaming);

useEffect(() => {
if (!isStreaming || effectiveApproval !== "needs-approval") return;
Expand Down Expand Up @@ -113,7 +117,7 @@ const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => {
<AIToolUseBatchItem key={idx} part={part} effectiveApproval={effectiveApproval} />
))}
</div>
{allNeedApproval && effectiveApproval === "needs-approval" && (
{effectiveApproval === "needs-approval" && (
<AIToolApprovalButtons count={parts.length} onApprove={handleApprove} onDeny={handleDeny} />
)}
</div>
Expand All @@ -139,7 +143,9 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => {
toolData.status === "completed" ? "text-success" : toolData.status === "error" ? "text-error" : "text-gray-400";

const baseApproval = userApprovalOverride || toolData.approval;
const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval;
const effectiveApproval = getEffectiveApprovalStatus(baseApproval, isStreaming);

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 +210,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 +231,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 +252,67 @@ 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 needsApproval = (part: WaveUIMessagePart & { type: "data-tooluse" }) => {
return getEffectiveApprovalStatus(part.data?.approval, isStreaming) === "needs-approval";
};

const readFileNeedsApproval: Array<WaveUIMessagePart & { type: "data-tooluse" }> = [];
const readFileOther: Array<WaveUIMessagePart & { type: "data-tooluse" }> = [];

for (const part of parts) {
if (isFileOp(part)) {
if (part.data.approval === "needs-approval") {
fileOpsNeedApproval.push(part);
if (needsApproval(part)) {
readFileNeedsApproval.push(part);
} else {
fileOpsNoApproval.push(part);
readFileOther.push(part);
}
} else {
otherTools.push(part);
}
}

const groupedItems: ToolGroupItem[] = [];
let addedApprovalBatch = false;
let addedOtherBatch = false;

for (const part of parts) {
const isFileOpPart = isFileOp(part);
const partNeedsApproval = needsApproval(part);

if (isFileOpPart && partNeedsApproval && !addedApprovalBatch) {
groupedItems.push({ type: "batch", parts: readFileNeedsApproval });
addedApprovalBatch = true;
} else if (isFileOpPart && !partNeedsApproval && !addedOtherBatch) {
groupedItems.push({ type: "batch", parts: readFileOther });
addedOtherBatch = true;
} else if (!isFileOpPart) {
groupedItems.push({ type: "single", part });
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<>
{fileOpsNoApproval.length > 0 && (
<div className="mt-2">
<AIToolUseBatch parts={fileOpsNoApproval} isStreaming={isStreaming} />
</div>
)}
{fileOpsNeedApproval.length > 0 && (
<div className="mt-2">
<AIToolUseBatch parts={fileOpsNeedApproval} 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>
)
)}
{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
Loading
Loading