Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions frontend/app/aipanel/aipanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ const AIPanelComponentInner = memo(() => {
return false;
};

useEffect(() => {
globalStore.set(model.isAIStreaming, status == "streaming");
}, [status]);

useEffect(() => {
const keyHandler = keydownWrapper(handleKeyDown);
document.addEventListener("keydown", keyHandler);
Expand Down
1 change: 1 addition & 0 deletions frontend/app/aipanel/waveai-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class WaveAIModel {
realMessage: AIMessage | null = null;
orefContext: ORef;
inBuilder: boolean = false;
isAIStreaming = jotai.atom(false);

widgetAccessAtom!: jotai.Atom<boolean>;
droppedFiles: jotai.PrimitiveAtom<DroppedFile[]> = jotai.atom([]);
Expand Down
5 changes: 5 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,11 @@ class RpcApiType {
return client.wshRpcCall("resolveids", data, opts);
}

// command "restartbuilderandwait" [call]
RestartBuilderAndWaitCommand(client: WshClient, data: CommandRestartBuilderAndWaitData, opts?: RpcOpts): Promise<RestartBuilderAndWaitResult> {
return client.wshRpcCall("restartbuilderandwait", data, opts);
}

// command "routeannounce" [call]
RouteAnnounceCommand(client: WshClient, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("routeannounce", null, opts);
Expand Down
1 change: 0 additions & 1 deletion frontend/builder/store/builder-apppanel-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ export class BuilderAppPanelModel {
scope: appId,
handler: () => {
this.loadAppFile(appId);
this.debouncedRestart();
},
});
}
Expand Down
41 changes: 41 additions & 0 deletions frontend/builder/tabs/builder-previewtab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { WaveAIModel } from "@/app/aipanel/waveai-model";
import { BuilderAppPanelModel } from "@/builder/store/builder-apppanel-model";
import { BuilderBuildPanelModel } from "@/builder/store/builder-buildpanel-model";
import { atoms } from "@/store/global";
import { useAtomValue } from "jotai";
import { memo, useState } from "react";
Expand Down Expand Up @@ -30,6 +32,29 @@ EmptyStateView.displayName = "EmptyStateView";

const ErrorStateView = memo(({ errorMsg }: { errorMsg: string }) => {
const displayMsg = errorMsg && errorMsg.trim() ? errorMsg : "Unknown Error";
const waveAIModel = WaveAIModel.getInstance();
const buildPanelModel = BuilderBuildPanelModel.getInstance();
const outputLines = useAtomValue(buildPanelModel.outputLines);
const isStreaming = useAtomValue(waveAIModel.isAIStreaming);

const getBuildContext = () => {
const filteredLines = outputLines.filter((line) => !line.startsWith("[debug]"));
const buildOutput = filteredLines.join("\n").trim();
return `Build Error:\n\`\`\`\n${displayMsg}\n\`\`\`\n\nBuild Output:\n\`\`\`\n${buildOutput}\n\`\`\``;
};

const handleAddToContext = () => {
const context = getBuildContext();
waveAIModel.appendText(context, true);
waveAIModel.focusInput();
};

const handleAskAIToFix = async () => {
const context = getBuildContext();
waveAIModel.appendText("Please help me fix this build error:\n\n" + context, true);
await waveAIModel.handleSubmit();
};

return (
<div className="w-full h-full flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-6 max-w-2xl text-center px-8">
Expand All @@ -38,6 +63,22 @@ const ErrorStateView = memo(({ errorMsg }: { errorMsg: string }) => {
<div className="text-left bg-panel border border-error/30 rounded-lg p-4 max-h-96 overflow-auto">
<pre className="text-sm text-secondary whitespace-pre-wrap font-mono">{displayMsg}</pre>
</div>
{!isStreaming && (
<div className="flex gap-3 mt-2 justify-center">
<button
onClick={handleAddToContext}
className="px-4 py-2 bg-panel text-primary border border-border rounded hover:bg-panel/80 transition-colors cursor-pointer"
>
Add Error to AI Context
</button>
<button
onClick={handleAskAIToFix}
className="px-4 py-2 bg-accent text-primary font-semibold rounded hover:bg-accent/80 transition-colors cursor-pointer"
>
Ask AI to Fix
</button>
</div>
)}
</div>
</div>
</div>
Expand Down
12 changes: 12 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,11 @@ declare global {
resolvedids: {[key: string]: ORef};
};

// wshrpc.CommandRestartBuilderAndWaitData
type CommandRestartBuilderAndWaitData = {
builderid: string;
};

// wshrpc.CommandSetMetaData
type CommandSetMetaData = {
oref: ORef;
Expand Down Expand Up @@ -909,6 +914,13 @@ declare global {
shell: string;
};

// wshrpc.RestartBuilderAndWaitResult
type RestartBuilderAndWaitResult = {
success: boolean;
errormessage?: string;
buildoutput: string;
};

// wshutil.RpcMessage
type RpcMessage = {
command?: string;
Expand Down
72 changes: 65 additions & 7 deletions pkg/aiusechat/tools_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@
package aiusechat

import (
"context"
"fmt"
"log"
"time"

"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
"github.com/wavetermdev/waveterm/pkg/buildercontroller"
"github.com/wavetermdev/waveterm/pkg/util/fileutil"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveappstore"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wstore"
)

const BuilderAppFileName = "app.go"
Expand All @@ -19,6 +25,35 @@ type builderWriteAppFileParams struct {
Contents string `json:"contents"`
}

func triggerBuildAndWait(builderId string, appId string) map[string]any {
bc := buildercontroller.GetOrCreateController(builderId)
rtInfo := wstore.GetRTInfo(waveobj.MakeORef(waveobj.OType_Builder, builderId))

var builderEnv map[string]string
if rtInfo != nil {
builderEnv = rtInfo.BuilderEnv
}

ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

result, err := bc.RestartAndWaitForBuild(ctx, appId, builderEnv)
if err != nil {
log.Printf("Build failed for %s: %v", builderId, err)
return map[string]any{
"build_success": false,
"build_error": err.Error(),
"build_output": "",
}
}

return map[string]any{
"build_success": result.Success,
"build_error": result.ErrorMessage,
"build_output": result.BuildOutput,
}
}

func parseBuilderWriteAppFileInput(input any) (*builderWriteAppFileParams, error) {
result := &builderWriteAppFileParams{}

Expand All @@ -37,7 +72,7 @@ func parseBuilderWriteAppFileInput(input any) (*builderWriteAppFileParams, error
return result, nil
}

func GetBuilderWriteAppFileToolDefinition(appId string) uctypes.ToolDefinition {
func GetBuilderWriteAppFileToolDefinition(appId string, builderId string) uctypes.ToolDefinition {
return uctypes.ToolDefinition{
Name: "builder_write_app_file",
DisplayName: "Write App File",
Expand Down Expand Up @@ -74,10 +109,19 @@ func GetBuilderWriteAppFileToolDefinition(appId string) uctypes.ToolDefinition {
Scopes: []string{appId},
})

return map[string]any{
result := map[string]any{
"success": true,
"message": fmt.Sprintf("Successfully wrote %s", BuilderAppFileName),
}, nil
}

if builderId != "" {
buildResult := triggerBuildAndWait(builderId, appId)
result["build_success"] = buildResult["build_success"]
result["build_error"] = buildResult["build_error"]
result["build_output"] = buildResult["build_output"]
}

return result, nil
},
}
}
Expand All @@ -104,7 +148,7 @@ func parseBuilderEditAppFileInput(input any) (*builderEditAppFileParams, error)
return result, nil
}

func GetBuilderEditAppFileToolDefinition(appId string) uctypes.ToolDefinition {
func GetBuilderEditAppFileToolDefinition(appId string, builderId string) uctypes.ToolDefinition {
return uctypes.ToolDefinition{
Name: "builder_edit_app_file",
DisplayName: "Edit App File",
Expand Down Expand Up @@ -147,7 +191,12 @@ func GetBuilderEditAppFileToolDefinition(appId string) uctypes.ToolDefinition {
if err != nil {
return fmt.Sprintf("error parsing input: %v", err)
}
return fmt.Sprintf("editing app.go for %s (%d edits)", appId, len(params.Edits))
numEdits := len(params.Edits)
editStr := "edits"
if numEdits == 1 {
editStr = "edit"
}
return fmt.Sprintf("editing app.go for %s (%d %s)", appId, numEdits, editStr)
},
ToolAnyCallback: func(input any, toolUseData *uctypes.UIMessageDataToolUse) (any, error) {
params, err := parseBuilderEditAppFileInput(input)
Expand All @@ -165,10 +214,19 @@ func GetBuilderEditAppFileToolDefinition(appId string) uctypes.ToolDefinition {
Scopes: []string{appId},
})

return map[string]any{
result := map[string]any{
"success": true,
"message": fmt.Sprintf("Successfully edited %s with %d changes", BuilderAppFileName, len(params.Edits)),
}, nil
}

if builderId != "" {
buildResult := triggerBuildAndWait(builderId, appId)
result["build_success"] = buildResult["build_success"]
result["build_error"] = buildResult["build_error"]
result["build_output"] = buildResult["build_output"]
}

return result, nil
},
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/aiusechat/usechat.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,8 +717,8 @@ func WaveAIPostMessageHandler(w http.ResponseWriter, r *http.Request) {

if req.BuilderAppId != "" {
chatOpts.Tools = append(chatOpts.Tools,
GetBuilderWriteAppFileToolDefinition(req.BuilderAppId),
GetBuilderEditAppFileToolDefinition(req.BuilderAppId),
GetBuilderWriteAppFileToolDefinition(req.BuilderAppId, req.BuilderId),
GetBuilderEditAppFileToolDefinition(req.BuilderAppId, req.BuilderId),
GetBuilderListFilesToolDefinition(req.BuilderAppId),
)
}
Expand Down
Loading
Loading