Skip to content

Commit 7ef2836

Browse files
committed
fix: address app studio preview review findings
1 parent c3ffc2b commit 7ef2836

16 files changed

Lines changed: 380 additions & 311 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,7 @@ uninstall-provider-app-studio: ## Delete App Studio CatalogEntry
11101110

11111111
# --- App Studio sandbox runner image (live development runtimes) ---
11121112
SANDBOX_RUNNER_IMAGE ?= ghcr.io/faroshq/kedge-sandbox-runner:dev
1113-
SANDBOX_TOKEN_GENERATOR_IMAGE ?= docker.io/bitnami/kubectl:latest
1113+
SANDBOX_TOKEN_GENERATOR_IMAGE ?= docker.io/bitnami/kubectl@sha256:b9f4412e53f09d76b0991cdd29c0feff4c1d1e112b307e0ab155e5b050a9f4ec
11141114
SANDBOX_RUNNER_PLATFORM ?= linux/$(ARCH)
11151115

11161116
docker-build-sandbox-runner: ## Build the App Studio sandbox runner image used by SandboxRunner pods

providers/app-studio/api/assistant_checkpoint.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import (
3030
aiv1alpha1 "github.com/faroshq/provider-app-studio/apis/ai/v1alpha1"
3131
asclient "github.com/faroshq/provider-app-studio/client"
3232
"github.com/faroshq/provider-app-studio/store"
33-
"github.com/faroshq/provider-app-studio/workspace"
3433
)
3534

3635
type projectAssistantCheckpointState struct {
@@ -135,8 +134,8 @@ func appendProjectAssistantResumePendingUI(out *projectAssistantResumeResponse,
135134
}
136135
}
137136

138-
func (s *Server) appendProjectAssistantResumeDevelopmentPreviewRefreshUI(ctx context.Context, out *projectAssistantResumeResponse, scope workspace.Scope, before string, beforeOK bool, toolCalls []projectToolCallStreamEvent) {
139-
if out == nil || !s.projectAssistantPreviewRefreshNeeded(ctx, scope, before, beforeOK, toolCalls) {
137+
func appendProjectAssistantResumeDevelopmentPreviewRefreshUI(out *projectAssistantResumeResponse, needed bool) {
138+
if out == nil || !needed {
140139
return
141140
}
142141
out.UIEvents = append(out.UIEvents, projectAssistantUIDevelopmentPreviewRefreshEvent())
@@ -577,12 +576,11 @@ func (s *Server) resumeClaimedProjectAssistantRunWithEinoCheckpoint(
577576
}
578577
currentRequestID := run.RequestID
579578
currentToolCallID := strings.TrimSpace(state.Eino.ToolCallID)
580-
workspaceDigestBefore, workspaceDigestErr := s.projectWorkspaceSyncDigest(ctx, engineReq.WorkspaceScope)
581-
workspaceDigestOK := workspaceDigestErr == nil
582579
result, err := s.projectAssistantEngine().ResumeProjectAssistant(ctx, engineReq, resumeReq, state)
583580
currentToolCall := projectAssistantResumeToolCall(streamedToolCalls, currentToolCallID)
584581
out.ToolCall = currentToolCall
585582
out.Result = projectAssistantResumeToolResult(result.Content, currentToolCall)
583+
previewRefreshNeeded := s.projectAssistantPreviewRefreshNeeded(ctx, engineReq.WorkspaceScope, "", false, streamedToolCalls)
586584
if err != nil {
587585
var permissionErr *projectAssistantPermissionRequiredError
588586
if !errors.As(err, &permissionErr) {
@@ -617,7 +615,7 @@ func (s *Server) resumeClaimedProjectAssistantRunWithEinoCheckpoint(
617615
assistantMessageID := strings.TrimSpace(resumeReq.AssistantMessageID)
618616
appendProjectAssistantResumeResolvedUI(&out, assistantMessageID, currentRequestID, currentToolCall)
619617
appendProjectAssistantResumePendingUI(&out, assistantMessageID)
620-
s.appendProjectAssistantResumeDevelopmentPreviewRefreshUI(ctx, &out, engineReq.WorkspaceScope, workspaceDigestBefore, workspaceDigestOK, streamedToolCalls)
618+
appendProjectAssistantResumeDevelopmentPreviewRefreshUI(&out, previewRefreshNeeded)
621619
messageUpdate := out
622620
messageUpdate.RunID = run.ID
623621
messageUpdate.RequestID = currentRequestID
@@ -665,7 +663,7 @@ func (s *Server) resumeClaimedProjectAssistantRunWithEinoCheckpoint(
665663
assistantMessageID := strings.TrimSpace(resumeReq.AssistantMessageID)
666664
appendProjectAssistantResumeResolvedUI(&out, assistantMessageID, currentRequestID, currentToolCall)
667665
appendProjectAssistantResumePendingUI(&out, assistantMessageID)
668-
s.appendProjectAssistantResumeDevelopmentPreviewRefreshUI(ctx, &out, engineReq.WorkspaceScope, workspaceDigestBefore, workspaceDigestOK, streamedToolCalls)
666+
appendProjectAssistantResumeDevelopmentPreviewRefreshUI(&out, previewRefreshNeeded)
669667
messageUpdate := out
670668
messageUpdate.RunID = run.ID
671669
messageUpdate.RequestID = currentRequestID
@@ -705,7 +703,7 @@ func (s *Server) resumeClaimedProjectAssistantRunWithEinoCheckpoint(
705703
}
706704
out.Status = run.Status
707705
appendProjectAssistantResumeResolvedUI(&out, strings.TrimSpace(resumeReq.AssistantMessageID), currentRequestID, currentToolCall)
708-
s.appendProjectAssistantResumeDevelopmentPreviewRefreshUI(ctx, &out, engineReq.WorkspaceScope, workspaceDigestBefore, workspaceDigestOK, streamedToolCalls)
706+
appendProjectAssistantResumeDevelopmentPreviewRefreshUI(&out, previewRefreshNeeded)
709707
if err := s.updateProjectAssistantPermissionMessage(persistCtx, messageScope, strings.TrimSpace(resumeReq.AssistantMessageID), out); err != nil {
710708
return projectAssistantResumeResponse{}, err
711709
}

providers/app-studio/api/assistant_eino_tool.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ func (t projectEinoAssistantTool) invokeAllowedTool(ctx context.Context, callID
195195
if err != nil {
196196
return t.finishFailedToolCall(callID, spec.Name, projectEinoToolArgumentsString(args), err.Error()), nil
197197
}
198+
if t.server != nil {
199+
t.server.scheduleDevelopmentSyncAfterMutation(t.req.Identity, t.req.Project, spec.Name)
200+
}
198201
t.emitToolCall(projectToolCallStreamEvent{
199202
ID: callID,
200203
Name: spec.Name,

providers/app-studio/api/assistant_eino_tool_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import (
2020
"context"
2121
"strings"
2222
"testing"
23+
24+
aiv1alpha1 "github.com/faroshq/provider-app-studio/apis/ai/v1alpha1"
25+
"github.com/faroshq/provider-app-studio/workspace"
2326
)
2427

2528
func TestEinoApprovePlanToolRejectsMissingAllowedOperations(t *testing.T) {
@@ -81,3 +84,43 @@ func TestEinoToolPassesSessionSnapshotToLocalTool(t *testing.T) {
8184
t.Fatal("tool received mutable run-state session snapshot")
8285
}
8386
}
87+
88+
func TestEinoToolSchedulesDevelopmentSyncAfterMutatingTool(t *testing.T) {
89+
runState := newProjectEinoAssistantRunState()
90+
server := &Server{}
91+
var gotName string
92+
var gotProjectName string
93+
server.developmentSyncAfterMutation = func(_ identity, p *aiv1alpha1.Project, name string) {
94+
gotName = name
95+
if p != nil {
96+
gotProjectName = p.Name
97+
}
98+
}
99+
localTool := projectAssistantToolFunc{
100+
spec: projectAssistantToolSpec{
101+
Name: projectToolWriteFile,
102+
Risk: projectAssistantToolRiskWrite,
103+
},
104+
call: func(context.Context, projectAssistantToolCallRequest) (string, error) {
105+
return `{"status":"ok"}`, nil
106+
},
107+
}
108+
project := &aiv1alpha1.Project{}
109+
project.Name = "demo"
110+
tool := projectEinoAssistantTool{
111+
server: server,
112+
tool: localTool,
113+
req: projectAssistantRunRequest{
114+
Project: project,
115+
WorkspaceScope: workspace.Scope{OrgUUID: "org-a", WorkspaceUUID: "ws-1", ProjectName: "demo"},
116+
},
117+
runState: runState,
118+
}
119+
120+
if _, err := tool.invokeAllowedTool(context.Background(), "call-write", localTool.Spec(), map[string]any{"path": "src/App.tsx"}); err != nil {
121+
t.Fatalf("invokeAllowedTool returned error: %v", err)
122+
}
123+
if gotName != projectToolWriteFile || gotProjectName != "demo" {
124+
t.Fatalf("scheduled sync = (%q, %q), want (%q, demo)", gotName, gotProjectName, projectToolWriteFile)
125+
}
126+
}

providers/app-studio/api/assistant_events.go

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ limitations under the License.
1616

1717
package api
1818

19-
import "context"
19+
import (
20+
"context"
21+
"strconv"
22+
)
2023

2124
type projectAssistantStreamWriter struct {
2225
assistantID string
2326
began bool
2427
rootChildren []string
2528
msgIdx int
26-
assistantContent string
2729
assistantDataKey string
2830
assistantShellSet bool
2931
pendingPermission *projectAssistantPermission
@@ -155,8 +157,7 @@ func (w *projectAssistantStreamWriter) writeAssistantContent(ctx context.Context
155157
}
156158
w.assistantShellSet = true
157159
}
158-
w.assistantContent += delta
159-
return w.write(projectMessageStreamEventFromUI(projectAssistantUIDataUpdateEvent(w.assistantID, w.assistantDataKey, w.assistantContent)))
160+
return w.write(projectMessageStreamEventFromUI(projectAssistantUIDataAppendEvent(w.assistantID, w.assistantDataKey, delta)))
160161
}
161162

162163
func (w *projectAssistantStreamWriter) writeToolCard(ctx context.Context, kind, text string) error {
@@ -212,28 +213,14 @@ func (w *projectAssistantStreamWriter) ensureBegin(ctx context.Context) error {
212213
func (w *projectAssistantStreamWriter) nextMessageComponentIDs() (string, string, string, string) {
213214
idx := w.msgIdx
214215
w.msgIdx++
215-
cardID := "msg-" + intString(idx) + "-card"
216-
colID := "msg-" + intString(idx) + "-col"
217-
labelID := "msg-" + intString(idx) + "-label"
218-
textID := "msg-" + intString(idx) + "-text"
216+
id := strconv.Itoa(idx)
217+
cardID := "msg-" + id + "-card"
218+
colID := "msg-" + id + "-col"
219+
labelID := "msg-" + id + "-label"
220+
textID := "msg-" + id + "-text"
219221
return cardID, colID, labelID, textID
220222
}
221223

222-
func intString(value int) string {
223-
if value == 0 {
224-
return "0"
225-
}
226-
const digits = "0123456789"
227-
var buf [20]byte
228-
i := len(buf)
229-
for value > 0 {
230-
i--
231-
buf[i] = digits[value%10]
232-
value /= 10
233-
}
234-
return string(buf[i:])
235-
}
236-
237224
func projectAssistantUIToolCardText(action projectAssistantUIAction) string {
238225
if action.Summary != "" {
239226
return action.Label + "\n" + action.Summary

providers/app-studio/api/assistant_events_test.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,37 @@ func TestProjectAssistantStreamWriterMapsDeltaToUIDataModelUpdate(t *testing.T)
4949
t.Fatalf("third event = %#v, want dataModelUpdate UI event", got[2])
5050
}
5151
contents := got[2].DataModelUpdate.Contents
52-
if len(contents) != 1 || contents[0].Key != "assistant-1/msg-0-text" || contents[0].ValueString != "hello" {
53-
t.Fatalf("data model contents = %#v, want assistant content binding", contents)
52+
if len(contents) != 1 || contents[0].Key != "assistant-1/msg-0-text" || contents[0].ValueString != "hello" || !contents[0].Append {
53+
t.Fatalf("data model contents = %#v, want appended assistant content binding", contents)
54+
}
55+
}
56+
57+
func TestProjectAssistantStreamWriterStreamsAssistantDeltasWithoutReplayingContent(t *testing.T) {
58+
got, err := collectProjectAssistantStreamEvents(
59+
projectAssistantEvent{
60+
Type: projectAssistantEventMessageDelta,
61+
Delta: "hello",
62+
},
63+
projectAssistantEvent{
64+
Type: projectAssistantEventMessageDelta,
65+
Delta: " world",
66+
},
67+
)
68+
if err != nil {
69+
t.Fatalf("EmitProjectAssistantEvent returned error: %v", err)
70+
}
71+
if len(got) != 4 {
72+
t.Fatalf("events = %#v, want beginRendering, shell, and two dataModelUpdates", got)
73+
}
74+
for i, want := range []string{"hello", " world"} {
75+
event := got[i+2]
76+
if event.DataModelUpdate == nil || len(event.DataModelUpdate.Contents) != 1 {
77+
t.Fatalf("event[%d] = %#v, want one dataModelUpdate content", i+2, event)
78+
}
79+
content := event.DataModelUpdate.Contents[0]
80+
if content.ValueString != want || !content.Append {
81+
t.Fatalf("event[%d] content = %#v, want append delta %q", i+2, content, want)
82+
}
5483
}
5584
}
5685

@@ -376,6 +405,7 @@ func TestStreamProjectAssistantEmitsCanonicalLifecycleSequence(t *testing.T) {
376405
"dataModelUpdate",
377406
"surfaceUpdate",
378407
"surfaceUpdate",
408+
"dataModelUpdate",
379409
"run_finished",
380410
}
381411
if len(got) != len(wantTypes) {
@@ -392,12 +422,15 @@ func TestStreamProjectAssistantEmitsCanonicalLifecycleSequence(t *testing.T) {
392422
assertA2UICard(t, got[1], "tool call", "Editing files")
393423
assertA2UICard(t, got[5], "tool call", "Editing files")
394424
assertA2UICard(t, got[6], "tool result", "Edited files")
395-
if got[7].Type != string(projectAssistantEventRunFinished) || got[7].AssistantMessageID != "assistant-sequence" {
396-
t.Fatalf("terminal event = %#v, want run_finished for assistant-sequence", got[7])
425+
if !projectMessageStreamEventsHaveContent([]projectMessageStreamEvent{got[7]}, projectAssistantUIDevelopmentPreviewRefreshKey) {
426+
t.Fatalf("preview refresh event = %#v, want preview refresh signal", got[7])
427+
}
428+
if got[8].Type != string(projectAssistantEventRunFinished) || got[8].AssistantMessageID != "assistant-sequence" {
429+
t.Fatalf("terminal event = %#v, want run_finished for assistant-sequence", got[8])
397430
}
398431
}
399432

400-
func TestStreamProjectAssistantEmitsPreviewRefreshWhenWorkspaceDigestChanges(t *testing.T) {
433+
func TestStreamProjectAssistantEmitsPreviewRefreshAfterMutatingToolCall(t *testing.T) {
401434
settings := projectLLMSettings{
402435
Provider: defaultProjectLLMProvider,
403436
BaseURL: defaultProjectLLMBaseURL,
@@ -519,6 +552,12 @@ func (projectAssistantWorkspaceMutationEngine) StreamProjectAssistant(ctx contex
519552
if _, err := req.Workspace.WriteFile(ctx, req.WorkspaceScope, workspace.WriteOptions{Path: "src/App.tsx", Content: "export function App() { return <main>Updated</main> }\n"}); err != nil {
520553
return projectAssistantRunResult{}, err
521554
}
555+
req.StreamCallbacks.OnToolCall(projectToolCallStreamEvent{
556+
ID: "tool-1",
557+
Name: projectToolWriteFile,
558+
Status: "succeeded",
559+
Summary: "Updated file",
560+
})
522561
req.StreamCallbacks.OnChunk("Updated the app.")
523562
return projectAssistantRunResult{Content: "Updated the app."}, nil
524563
}

providers/app-studio/api/assistant_ui_events.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ type projectAssistantUIDataModelUpdate struct {
9595
type projectAssistantUIDataContent struct {
9696
Key string `json:"key"`
9797
ValueString string `json:"valueString,omitempty"`
98+
Append bool `json:"append,omitempty"`
9899
}
99100

100101
type projectAssistantUIInterruptRequest struct {
@@ -123,12 +124,21 @@ func projectAssistantUIBeginRenderingEvent(surfaceID string) projectAssistantUIE
123124
}
124125

125126
func projectAssistantUIDataUpdateEvent(surfaceID, key, value string) projectAssistantUIEvent {
127+
return projectAssistantUIDataContentEvent(surfaceID, key, value, false)
128+
}
129+
130+
func projectAssistantUIDataAppendEvent(surfaceID, key, value string) projectAssistantUIEvent {
131+
return projectAssistantUIDataContentEvent(surfaceID, key, value, true)
132+
}
133+
134+
func projectAssistantUIDataContentEvent(surfaceID, key, value string, appendValue bool) projectAssistantUIEvent {
126135
return projectAssistantUIEvent{
127136
DataModelUpdate: &projectAssistantUIDataModelUpdate{
128137
SurfaceID: surfaceID,
129138
Contents: []projectAssistantUIDataContent{{
130139
Key: key,
131140
ValueString: value,
141+
Append: appendValue,
132142
}},
133143
},
134144
}

0 commit comments

Comments
 (0)