Skip to content

Commit 0da0a64

Browse files
authored
when the AI edits a file it now triggers a rebuild and the AI gets the tool output (#2546)
1 parent eb3ba64 commit 0da0a64

File tree

12 files changed

+280
-32
lines changed

12 files changed

+280
-32
lines changed

frontend/app/aipanel/aipanel.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ const AIPanelComponentInner = memo(() => {
254254
return false;
255255
};
256256

257+
useEffect(() => {
258+
globalStore.set(model.isAIStreaming, status == "streaming");
259+
}, [status]);
260+
257261
useEffect(() => {
258262
const keyHandler = keydownWrapper(handleKeyDown);
259263
document.addEventListener("keydown", keyHandler);

frontend/app/aipanel/waveai-model.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export class WaveAIModel {
5252
realMessage: AIMessage | null = null;
5353
orefContext: ORef;
5454
inBuilder: boolean = false;
55+
isAIStreaming = jotai.atom(false);
5556

5657
widgetAccessAtom!: jotai.Atom<boolean>;
5758
droppedFiles: jotai.PrimitiveAtom<DroppedFile[]> = jotai.atom([]);

frontend/app/store/wshclientapi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,11 @@ class RpcApiType {
472472
return client.wshRpcCall("resolveids", data, opts);
473473
}
474474

475+
// command "restartbuilderandwait" [call]
476+
RestartBuilderAndWaitCommand(client: WshClient, data: CommandRestartBuilderAndWaitData, opts?: RpcOpts): Promise<RestartBuilderAndWaitResult> {
477+
return client.wshRpcCall("restartbuilderandwait", data, opts);
478+
}
479+
475480
// command "routeannounce" [call]
476481
RouteAnnounceCommand(client: WshClient, opts?: RpcOpts): Promise<void> {
477482
return client.wshRpcCall("routeannounce", null, opts);

frontend/builder/store/builder-apppanel-model.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ export class BuilderAppPanelModel {
106106
scope: appId,
107107
handler: () => {
108108
this.loadAppFile(appId);
109-
this.debouncedRestart();
110109
},
111110
});
112111
}

frontend/builder/tabs/builder-previewtab.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

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

3133
const ErrorStateView = memo(({ errorMsg }: { errorMsg: string }) => {
3234
const displayMsg = errorMsg && errorMsg.trim() ? errorMsg : "Unknown Error";
35+
const waveAIModel = WaveAIModel.getInstance();
36+
const buildPanelModel = BuilderBuildPanelModel.getInstance();
37+
const outputLines = useAtomValue(buildPanelModel.outputLines);
38+
const isStreaming = useAtomValue(waveAIModel.isAIStreaming);
39+
40+
const getBuildContext = () => {
41+
const filteredLines = outputLines.filter((line) => !line.startsWith("[debug]"));
42+
const buildOutput = filteredLines.join("\n").trim();
43+
return `Build Error:\n\`\`\`\n${displayMsg}\n\`\`\`\n\nBuild Output:\n\`\`\`\n${buildOutput}\n\`\`\``;
44+
};
45+
46+
const handleAddToContext = () => {
47+
const context = getBuildContext();
48+
waveAIModel.appendText(context, true);
49+
waveAIModel.focusInput();
50+
};
51+
52+
const handleAskAIToFix = async () => {
53+
const context = getBuildContext();
54+
waveAIModel.appendText("Please help me fix this build error:\n\n" + context, true);
55+
await waveAIModel.handleSubmit();
56+
};
57+
3358
return (
3459
<div className="w-full h-full flex items-center justify-center bg-background">
3560
<div className="flex flex-col items-center gap-6 max-w-2xl text-center px-8">
@@ -38,6 +63,22 @@ const ErrorStateView = memo(({ errorMsg }: { errorMsg: string }) => {
3863
<div className="text-left bg-panel border border-error/30 rounded-lg p-4 max-h-96 overflow-auto">
3964
<pre className="text-sm text-secondary whitespace-pre-wrap font-mono">{displayMsg}</pre>
4065
</div>
66+
{!isStreaming && (
67+
<div className="flex gap-3 mt-2 justify-center">
68+
<button
69+
onClick={handleAddToContext}
70+
className="px-4 py-2 bg-panel text-primary border border-border rounded hover:bg-panel/80 transition-colors cursor-pointer"
71+
>
72+
Add Error to AI Context
73+
</button>
74+
<button
75+
onClick={handleAskAIToFix}
76+
className="px-4 py-2 bg-accent text-primary font-semibold rounded hover:bg-accent/80 transition-colors cursor-pointer"
77+
>
78+
Ask AI to Fix
79+
</button>
80+
</div>
81+
)}
4182
</div>
4283
</div>
4384
</div>

frontend/types/gotypes.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,11 @@ declare global {
379379
resolvedids: {[key: string]: ORef};
380380
};
381381

382+
// wshrpc.CommandRestartBuilderAndWaitData
383+
type CommandRestartBuilderAndWaitData = {
384+
builderid: string;
385+
};
386+
382387
// wshrpc.CommandSetMetaData
383388
type CommandSetMetaData = {
384389
oref: ORef;
@@ -909,6 +914,13 @@ declare global {
909914
shell: string;
910915
};
911916

917+
// wshrpc.RestartBuilderAndWaitResult
918+
type RestartBuilderAndWaitResult = {
919+
success: boolean;
920+
errormessage?: string;
921+
buildoutput: string;
922+
};
923+
912924
// wshutil.RpcMessage
913925
type RpcMessage = {
914926
command?: string;

pkg/aiusechat/tools_builder.go

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
package aiusechat
55

66
import (
7+
"context"
78
"fmt"
9+
"log"
10+
"time"
811

912
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
13+
"github.com/wavetermdev/waveterm/pkg/buildercontroller"
1014
"github.com/wavetermdev/waveterm/pkg/util/fileutil"
1115
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
1216
"github.com/wavetermdev/waveterm/pkg/waveappstore"
17+
"github.com/wavetermdev/waveterm/pkg/waveobj"
1318
"github.com/wavetermdev/waveterm/pkg/wps"
19+
"github.com/wavetermdev/waveterm/pkg/wstore"
1420
)
1521

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

28+
func triggerBuildAndWait(builderId string, appId string) map[string]any {
29+
bc := buildercontroller.GetOrCreateController(builderId)
30+
rtInfo := wstore.GetRTInfo(waveobj.MakeORef(waveobj.OType_Builder, builderId))
31+
32+
var builderEnv map[string]string
33+
if rtInfo != nil {
34+
builderEnv = rtInfo.BuilderEnv
35+
}
36+
37+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
38+
defer cancel()
39+
40+
result, err := bc.RestartAndWaitForBuild(ctx, appId, builderEnv)
41+
if err != nil {
42+
log.Printf("Build failed for %s: %v", builderId, err)
43+
return map[string]any{
44+
"build_success": false,
45+
"build_error": err.Error(),
46+
"build_output": "",
47+
}
48+
}
49+
50+
return map[string]any{
51+
"build_success": result.Success,
52+
"build_error": result.ErrorMessage,
53+
"build_output": result.BuildOutput,
54+
}
55+
}
56+
2257
func parseBuilderWriteAppFileInput(input any) (*builderWriteAppFileParams, error) {
2358
result := &builderWriteAppFileParams{}
2459

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

40-
func GetBuilderWriteAppFileToolDefinition(appId string) uctypes.ToolDefinition {
75+
func GetBuilderWriteAppFileToolDefinition(appId string, builderId string) uctypes.ToolDefinition {
4176
return uctypes.ToolDefinition{
4277
Name: "builder_write_app_file",
4378
DisplayName: "Write App File",
@@ -74,10 +109,19 @@ func GetBuilderWriteAppFileToolDefinition(appId string) uctypes.ToolDefinition {
74109
Scopes: []string{appId},
75110
})
76111

77-
return map[string]any{
112+
result := map[string]any{
78113
"success": true,
79114
"message": fmt.Sprintf("Successfully wrote %s", BuilderAppFileName),
80-
}, nil
115+
}
116+
117+
if builderId != "" {
118+
buildResult := triggerBuildAndWait(builderId, appId)
119+
result["build_success"] = buildResult["build_success"]
120+
result["build_error"] = buildResult["build_error"]
121+
result["build_output"] = buildResult["build_output"]
122+
}
123+
124+
return result, nil
81125
},
82126
}
83127
}
@@ -104,7 +148,7 @@ func parseBuilderEditAppFileInput(input any) (*builderEditAppFileParams, error)
104148
return result, nil
105149
}
106150

107-
func GetBuilderEditAppFileToolDefinition(appId string) uctypes.ToolDefinition {
151+
func GetBuilderEditAppFileToolDefinition(appId string, builderId string) uctypes.ToolDefinition {
108152
return uctypes.ToolDefinition{
109153
Name: "builder_edit_app_file",
110154
DisplayName: "Edit App File",
@@ -147,7 +191,12 @@ func GetBuilderEditAppFileToolDefinition(appId string) uctypes.ToolDefinition {
147191
if err != nil {
148192
return fmt.Sprintf("error parsing input: %v", err)
149193
}
150-
return fmt.Sprintf("editing app.go for %s (%d edits)", appId, len(params.Edits))
194+
numEdits := len(params.Edits)
195+
editStr := "edits"
196+
if numEdits == 1 {
197+
editStr = "edit"
198+
}
199+
return fmt.Sprintf("editing app.go for %s (%d %s)", appId, numEdits, editStr)
151200
},
152201
ToolAnyCallback: func(input any, toolUseData *uctypes.UIMessageDataToolUse) (any, error) {
153202
params, err := parseBuilderEditAppFileInput(input)
@@ -165,10 +214,19 @@ func GetBuilderEditAppFileToolDefinition(appId string) uctypes.ToolDefinition {
165214
Scopes: []string{appId},
166215
})
167216

168-
return map[string]any{
217+
result := map[string]any{
169218
"success": true,
170219
"message": fmt.Sprintf("Successfully edited %s with %d changes", BuilderAppFileName, len(params.Edits)),
171-
}, nil
220+
}
221+
222+
if builderId != "" {
223+
buildResult := triggerBuildAndWait(builderId, appId)
224+
result["build_success"] = buildResult["build_success"]
225+
result["build_error"] = buildResult["build_error"]
226+
result["build_output"] = buildResult["build_output"]
227+
}
228+
229+
return result, nil
172230
},
173231
}
174232
}

pkg/aiusechat/usechat.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -717,8 +717,8 @@ func WaveAIPostMessageHandler(w http.ResponseWriter, r *http.Request) {
717717

718718
if req.BuilderAppId != "" {
719719
chatOpts.Tools = append(chatOpts.Tools,
720-
GetBuilderWriteAppFileToolDefinition(req.BuilderAppId),
721-
GetBuilderEditAppFileToolDefinition(req.BuilderAppId),
720+
GetBuilderWriteAppFileToolDefinition(req.BuilderAppId, req.BuilderId),
721+
GetBuilderEditAppFileToolDefinition(req.BuilderAppId, req.BuilderId),
722722
GetBuilderListFilesToolDefinition(req.BuilderAppId),
723723
)
724724
}

0 commit comments

Comments
 (0)