From b461ac05e70425347fa1c6a72c0c823137f91f54 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Wed, 13 May 2026 12:51:26 +0000 Subject: [PATCH 1/4] fix(orchestrator): localize intent confirmation bubble Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: jinyu918 <183728970+jinyu918@users.noreply.github.com> --- .../internal/orchestrator/memory_handoffs.go | 37 ++------ .../internal/orchestrator/service_test.go | 24 ++--- .../internal/orchestrator/task_confirm.go | 12 +-- .../task_confirm_language_test.go | 82 ++++++++++++++++ .../orchestrator/task_confirm_prompt.go | 93 +++++++++++++++---- 5 files changed, 181 insertions(+), 67 deletions(-) create mode 100644 services/local-service/internal/orchestrator/task_confirm_language_test.go diff --git a/services/local-service/internal/orchestrator/memory_handoffs.go b/services/local-service/internal/orchestrator/memory_handoffs.go index a60584be..de077bb2 100644 --- a/services/local-service/internal/orchestrator/memory_handoffs.go +++ b/services/local-service/internal/orchestrator/memory_handoffs.go @@ -307,23 +307,8 @@ func readPlanRetrievalContextItems(plan map[string]any) []map[string]any { } } -func clarificationBubbleTextForLanguage(suggestionIntent map[string]any, hits []memory.RetrievalHit, replyLanguage string) string { - base := clarificationBaseTextForLanguage(suggestionIntent, replyLanguage) - if len(hits) == 0 { - return base - } - - summary := strings.TrimSpace(hits[0].Summary) - if summary == "" { - return base - } - - trimmedSummary := truncateText(summary, 72) - if replyLanguage == languagepolicy.ReplyLanguageEnglish { - return fmt.Sprintf("Based on your earlier context (%s), %s", trimmedSummary, clarificationFollowUpPrompt(suggestionIntent, true)) - } - - return fmt.Sprintf("结合你之前提到的内容(%s),%s", trimmedSummary, clarificationFollowUpPrompt(suggestionIntent, false)) +func clarificationBubbleTextForLanguage(suggestionIntent map[string]any, _ []memory.RetrievalHit, replyLanguage string) string { + return clarificationBaseTextForLanguage(suggestionIntent, replyLanguage) } func clarificationBaseTextForLanguage(suggestionIntent map[string]any, replyLanguage string) string { @@ -335,7 +320,11 @@ func clarificationBaseTextForLanguage(suggestionIntent map[string]any, replyLang func initialClarificationPromptForLanguage(snapshot taskcontext.TaskContextSnapshot, startFlow bool, replyLanguage string) string { if strings.TrimSpace(replyLanguage) == "" { - replyLanguage = clarificationReplyLanguage(snapshot) + preferredInput := firstNonEmptyString(snapshot.Text, snapshot.ErrorText) + if preferredInput == "" { + preferredInput = firstNonEmptyString(snapshot.SelectionText, memoryQueryFromSnapshot(snapshot)) + } + replyLanguage = languagepolicy.PreferredReplyLanguage(preferredInput) } if replyLanguage == languagepolicy.ReplyLanguageEnglish { if startFlow { @@ -349,18 +338,6 @@ func initialClarificationPromptForLanguage(snapshot taskcontext.TaskContextSnaps return "我还不确定你想如何处理这段内容,请确认目标。" } -func clarificationReplyLanguage(snapshot taskcontext.TaskContextSnapshot) string { - preferredInput := firstNonEmptyString(snapshot.Text, snapshot.ErrorText) - if preferredInput == "" { - preferredInput = firstNonEmptyString(snapshot.SelectionText, memoryQueryFromSnapshot(snapshot)) - } - currentLanguage := languagepolicy.PreferredReplyLanguage(preferredInput) - if shouldPreferRememberedSessionLanguage(snapshot, currentLanguage) { - return strings.TrimSpace(snapshot.SessionReplyLanguage) - } - return currentLanguage -} - func clarificationFollowUpPrompt(taskIntent map[string]any, english bool) string { switch stringValue(taskIntent, "name", "") { case "translate": diff --git a/services/local-service/internal/orchestrator/service_test.go b/services/local-service/internal/orchestrator/service_test.go index ee95f95b..b47d1b9c 100644 --- a/services/local-service/internal/orchestrator/service_test.go +++ b/services/local-service/internal/orchestrator/service_test.go @@ -11302,7 +11302,7 @@ func TestServiceConfirmTaskReusesRetrievalHitsAfterConfirmation(t *testing.T) { } } -func TestServiceSubmitInputClarificationIncludesRetrievedMemoryContext(t *testing.T) { +func TestServiceSubmitInputClarificationDoesNotExposeRetrievedMemoryContext(t *testing.T) { service, _ := newTestServiceWithModelClient(t, stubModelClient{ output: `{"route":"clarification_needed","reply":""}`, }) @@ -11332,11 +11332,11 @@ func TestServiceSubmitInputClarificationIncludesRetrievedMemoryContext(t *testin bubble := result["bubble_message"].(map[string]any) text := stringValue(bubble, "text", "") - if !strings.Contains(text, "Based on your earlier context") { - t.Fatalf("expected clarification bubble to mention prior memory context, got %+v", bubble) + if strings.Contains(text, "Based on your earlier context") { + t.Fatalf("expected clarification bubble to hide prior memory framing, got %+v", bubble) } - if !strings.Contains(text, "project alpha focuses on rollout notes") { - t.Fatalf("expected clarification bubble to include retrieved summary, got %+v", bubble) + if strings.Contains(text, "project alpha focuses on rollout notes") { + t.Fatalf("expected clarification bubble to hide retrieved summary, got %+v", bubble) } } @@ -11375,11 +11375,8 @@ func TestServiceClarificationReusesMaterializedMemoryHits(t *testing.T) { startBubble := result["bubble_message"].(map[string]any) startText := stringValue(startBubble, "text", "") - if !strings.Contains(startText, "first retrieved summary should stay stable") { - t.Fatalf("expected start clarification bubble to reuse stored retrieval hit, got %+v", startBubble) - } - if strings.Contains(startText, "second search result should not replace clarification context") { - t.Fatalf("expected start clarification bubble to avoid fresh search drift, got %+v", startBubble) + if strings.Contains(startText, "first retrieved summary should stay stable") || strings.Contains(startText, "second search result should not replace clarification context") { + t.Fatalf("expected start clarification bubble to hide retrieved summaries, got %+v", startBubble) } taskID := result["task"].(map[string]any)["task_id"].(string) @@ -11393,11 +11390,8 @@ func TestServiceClarificationReusesMaterializedMemoryHits(t *testing.T) { confirmBubble := confirmResult["bubble_message"].(map[string]any) confirmText := stringValue(confirmBubble, "text", "") - if !strings.Contains(confirmText, "first retrieved summary should stay stable") { - t.Fatalf("expected confirm clarification bubble to reuse stored retrieval hit, got %+v", confirmBubble) - } - if strings.Contains(confirmText, "second search result should not replace clarification context") { - t.Fatalf("expected confirm clarification bubble to avoid fresh search drift, got %+v", confirmBubble) + if strings.Contains(confirmText, "first retrieved summary should stay stable") || strings.Contains(confirmText, "second search result should not replace clarification context") { + t.Fatalf("expected confirm clarification bubble to hide retrieved summaries, got %+v", confirmBubble) } if memoryStore.SearchCalls() != 1 { t.Fatalf("expected clarification flow to reuse materialized hits without re-searching, calls=%d", memoryStore.SearchCalls()) diff --git a/services/local-service/internal/orchestrator/task_confirm.go b/services/local-service/internal/orchestrator/task_confirm.go index 1b600bd1..a32f2380 100644 --- a/services/local-service/internal/orchestrator/task_confirm.go +++ b/services/local-service/internal/orchestrator/task_confirm.go @@ -27,7 +27,7 @@ func (s *Service) ConfirmTask(params map[string]any) (map[string]any, error) { return nil, err } snapshot := snapshotFromTask(task) - replyLanguage := taskConfirmReplyLanguage(snapshot, correctionText) + replyLanguage := s.taskConfirmReplyLanguage(snapshot, correctionText) intentValue := cloneMap(task.Intent) updatedTitle := task.Title if !confirmed && correctionText != "" { @@ -45,7 +45,7 @@ func (s *Service) ConfirmTask(params map[string]any) (map[string]any, error) { return nil, err } snapshot := snapshotFromTask(updatedTask) - replyLanguage := taskConfirmReplyLanguage(snapshot, correctionText) + replyLanguage := s.taskConfirmReplyLanguage(snapshot, correctionText) clarificationText := rejectedIntentClarificationText(replyLanguage) if clarificationHits := s.clarificationPreviewHits(updatedTask, snapshot); len(clarificationHits) > 0 { clarificationText = clarificationText + " " + clarificationBubbleTextForLanguage(map[string]any{}, clarificationHits, replyLanguage) @@ -68,7 +68,7 @@ func (s *Service) ConfirmTask(params map[string]any) (map[string]any, error) { return nil, err } snapshot := snapshotFromTask(updatedTask) - replyLanguage := taskConfirmReplyLanguage(snapshot, correctionText) + replyLanguage := s.taskConfirmReplyLanguage(snapshot, correctionText) clarificationText := rejectedIntentClarificationText(replyLanguage) if clarificationHits := s.clarificationPreviewHits(updatedTask, snapshot); len(clarificationHits) > 0 { clarificationText = clarificationText + " " + clarificationBubbleTextForLanguage(map[string]any{}, clarificationHits, replyLanguage) @@ -87,7 +87,7 @@ func (s *Service) ConfirmTask(params map[string]any) (map[string]any, error) { } if strings.TrimSpace(stringValue(intentValue, "name", "")) == "" { snapshot := snapshotFromTask(task) - replyLanguage := taskConfirmReplyLanguage(snapshot, correctionText) + replyLanguage := s.taskConfirmReplyLanguage(snapshot, correctionText) clarificationText := missingIntentClarificationText(replyLanguage) if clarificationHits := s.clarificationPreviewHits(task, snapshot); len(clarificationHits) > 0 { clarificationText = clarificationText + " " + clarificationBubbleTextForLanguage(map[string]any{}, clarificationHits, replyLanguage) @@ -157,11 +157,11 @@ func confirmationAcceptedText(replyLanguage string) string { return "已按新的要求开始处理" } -func taskConfirmReplyLanguage(snapshot taskcontext.TaskContextSnapshot, correctionText string) string { +func (s *Service) taskConfirmReplyLanguage(snapshot taskcontext.TaskContextSnapshot, correctionText string) string { if trimmed := strings.TrimSpace(correctionText); trimmed != "" { return languagepolicy.PreferredReplyLanguage(trimmed) } - return clarificationReplyLanguage(snapshot) + return s.confirmationReplyLanguage(snapshot) } func isEnglishReplyLanguage(replyLanguage string) bool { diff --git a/services/local-service/internal/orchestrator/task_confirm_language_test.go b/services/local-service/internal/orchestrator/task_confirm_language_test.go new file mode 100644 index 00000000..81bac08e --- /dev/null +++ b/services/local-service/internal/orchestrator/task_confirm_language_test.go @@ -0,0 +1,82 @@ +package orchestrator + +import ( + "strings" + "testing" + + "github.com/cialloclaw/cialloclaw/services/local-service/internal/intent" + "github.com/cialloclaw/cialloclaw/services/local-service/internal/languagepolicy" + "github.com/cialloclaw/cialloclaw/services/local-service/internal/runengine" + "github.com/cialloclaw/cialloclaw/services/local-service/internal/taskcontext" +) + +func TestBubbleTextForConfirmationUsesSettingsDefaultLanguage(t *testing.T) { + service, _ := newTestServiceWithModelClient(t, stubModelClient{ + output: `{"intent":{"name":"summarize","arguments":{"style":"brief"}}}`, + }) + if _, _, _, _, err := service.runEngine.UpdateSettings(map[string]any{ + "general": map[string]any{"language": languagepolicy.ReplyLanguageEnglish}, + }); err != nil { + t.Fatalf("update settings failed: %v", err) + } + + text := service.bubbleTextForConfirmation( + runengine.TaskRecord{}, + taskcontext.TaskContextSnapshot{}, + intent.Suggestion{RequiresConfirm: true}, + true, + ) + if !strings.Contains(text, "Please confirm the goal first.") { + t.Fatalf("expected english clarification bubble from settings default, got %q", text) + } +} + +func TestBubbleTextForConfirmationPrefersRememberedSessionLanguage(t *testing.T) { + service, _ := newTestServiceWithModelClient(t, stubModelClient{ + output: `{"intent":{"name":"summarize","arguments":{"style":"brief"}}}`, + }) + if _, _, _, _, err := service.runEngine.UpdateSettings(map[string]any{ + "general": map[string]any{"language": languagepolicy.ReplyLanguageEnglish}, + }); err != nil { + t.Fatalf("update settings failed: %v", err) + } + + text := service.bubbleTextForConfirmation( + runengine.TaskRecord{}, + taskcontext.TaskContextSnapshot{ + InputType: "text", + Text: "open it", + SessionReplyLanguage: languagepolicy.ReplyLanguageChinese, + SessionContextText: "请继续上一段中文任务上下文", + }, + intent.Suggestion{RequiresConfirm: true}, + true, + ) + if !strings.Contains(text, "请先确认") { + t.Fatalf("expected remembered chinese session language to override default english, got %q", text) + } +} + +func TestBuildTaskConfirmQuestionPromptUsesReplyLanguage(t *testing.T) { + snapshot := taskcontext.TaskContextSnapshot{Text: "openclaw docs"} + suggestion := intent.Suggestion{ + TaskTitle: "Open docs", + TaskSourceType: "floating_ball", + DirectDeliveryType: "bubble", + Intent: map[string]any{ + "name": "open_page", + "arguments": map[string]any{ + "url": "https://openclaw.example", + }, + }, + } + + englishPrompt := buildTaskConfirmQuestionPrompt(snapshot, suggestion, languagepolicy.ReplyLanguageEnglish) + if !strings.Contains(englishPrompt, "Return exactly one concise English question and nothing else.") { + t.Fatalf("expected english confirm prompt instruction, got %q", englishPrompt) + } + chinesePrompt := buildTaskConfirmQuestionPrompt(snapshot, suggestion, languagepolicy.ReplyLanguageChinese) + if !strings.Contains(chinesePrompt, "Return exactly one concise Chinese question and nothing else.") { + t.Fatalf("expected chinese confirm prompt instruction, got %q", chinesePrompt) + } +} diff --git a/services/local-service/internal/orchestrator/task_confirm_prompt.go b/services/local-service/internal/orchestrator/task_confirm_prompt.go index 3efb7a67..ad694493 100644 --- a/services/local-service/internal/orchestrator/task_confirm_prompt.go +++ b/services/local-service/internal/orchestrator/task_confirm_prompt.go @@ -9,6 +9,7 @@ import ( "time" "github.com/cialloclaw/cialloclaw/services/local-service/internal/intent" + "github.com/cialloclaw/cialloclaw/services/local-service/internal/languagepolicy" "github.com/cialloclaw/cialloclaw/services/local-service/internal/model" "github.com/cialloclaw/cialloclaw/services/local-service/internal/presentation" "github.com/cialloclaw/cialloclaw/services/local-service/internal/runengine" @@ -27,14 +28,15 @@ func (s *Service) bubbleTextForStart(task runengine.TaskRecord, snapshot taskcon } func (s *Service) bubbleTextForConfirmation(task runengine.TaskRecord, snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion, startFlow bool) string { + replyLanguage := s.confirmationReplyLanguage(snapshot) if !suggestion.RequiresConfirm { return suggestion.ResultBubbleText } if shouldUseClarificationBubble(suggestion) { if !suggestion.IntentConfirmed { - return initialClarificationPromptForLanguage(snapshot, startFlow, snapshot.SessionReplyLanguage) + return initialClarificationPromptForLanguage(snapshot, startFlow, replyLanguage) } - return clarificationBubbleTextForLanguage(suggestion.Intent, s.clarificationPreviewHits(task, snapshot), clarificationReplyLanguage(snapshot)) + return clarificationBubbleTextForLanguage(suggestion.Intent, s.clarificationPreviewHits(task, snapshot), replyLanguage) } if !suggestion.IntentConfirmed { if startFlow { @@ -57,13 +59,14 @@ func shouldUseClarificationBubble(suggestion intent.Suggestion) bool { // runtime model is unavailable or returns unusable text, the backend falls back // to a deterministic question so the confirmation gate remains stable. func (s *Service) confirmIntentText(snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion) string { - if question := s.modelBackedConfirmIntentText(snapshot, suggestion); question != "" { + replyLanguage := s.confirmationReplyLanguage(snapshot) + if question := s.modelBackedConfirmIntentText(snapshot, suggestion, replyLanguage); question != "" { return question } - return fallbackConfirmIntentText(snapshot, suggestion) + return fallbackConfirmIntentText(snapshot, suggestion, replyLanguage) } -func (s *Service) modelBackedConfirmIntentText(snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion) string { +func (s *Service) modelBackedConfirmIntentText(snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion, replyLanguage string) string { modelService := s.currentModel() if modelService == nil || !shouldUseModelBackedConfirmIntentText(suggestion) { return "" @@ -72,7 +75,7 @@ func (s *Service) modelBackedConfirmIntentText(snapshot taskcontext.TaskContextS ctx, cancel := context.WithTimeout(context.Background(), taskConfirmQuestionModelTimeout) defer cancel() response, err := modelService.GenerateText(ctx, model.GenerateTextRequest{ - Input: buildTaskConfirmQuestionPrompt(snapshot, suggestion), + Input: buildTaskConfirmQuestionPrompt(snapshot, suggestion, replyLanguage), }) if err != nil { return "" @@ -96,7 +99,7 @@ func shouldUseModelBackedConfirmIntentText(suggestion intent.Suggestion) bool { // runtime model is unavailable. It still derives the question from the formal // task title and current object context instead of restoring a frontend phrase // table or a per-intent copy map. -func fallbackConfirmIntentText(snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion) string { +func fallbackConfirmIntentText(snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion, replyLanguage string) string { action, subject := intentConfirmTitleParts(suggestion.TaskTitle) if subject == "" { action = intentConfirmAction(snapshot, suggestion.Intent) @@ -105,10 +108,19 @@ func fallbackConfirmIntentText(snapshot taskcontext.TaskContextSnapshot, suggest switch { case action != "" && subject != "": + if isEnglishReplyLanguage(replyLanguage) { + return fmt.Sprintf("Do you want me to %s \"%s\"?", action, subject) + } return fmt.Sprintf("你现在是希望我%s「%s」吗?", action, subject) case action != "": + if isEnglishReplyLanguage(replyLanguage) { + return fmt.Sprintf("Do you want me to %s?", action) + } return fmt.Sprintf("你现在是希望我%s吗?", action) default: + if isEnglishReplyLanguage(replyLanguage) { + return "Please confirm how you want me to handle this content." + } return presentation.Text(presentation.MessageBubbleConfirmDefault, nil) } } @@ -131,15 +143,10 @@ func intentConfirmAction(snapshot taskcontext.TaskContextSnapshot, taskIntent ma return strings.TrimSpace(strings.TrimRight(prefix, "::")) } -func buildTaskConfirmQuestionPrompt(snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion) string { +func buildTaskConfirmQuestionPrompt(snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion, replyLanguage string) string { intentPayload, _ := json.Marshal(suggestion.Intent) - lines := []string{ - "You write one confirmation question for CialloClaw before the task executes.", - "Return exactly one concise Chinese question and nothing else.", - "Use the inferred operation and the most specific target object you can justify from the intent payload and task context.", - "Do not mention internal labels such as intent, task title, current intent, JSON, payload, or field names.", - "Do not explain your reasoning. Do not add bullets, quotes, or multiple options.", - "If the task changes files, pages, or other state, make that action explicit in the question.", + lines := taskConfirmQuestionPromptInstructions(replyLanguage) + lines = append(lines, "", fmt.Sprintf("task_title=%s", strings.TrimSpace(suggestion.TaskTitle)), fmt.Sprintf("intent_payload=%s", string(intentPayload)), @@ -148,10 +155,64 @@ func buildTaskConfirmQuestionPrompt(snapshot taskcontext.TaskContextSnapshot, su "", "task_context:", taskConfirmQuestionContextSummary(snapshot), - } + ) return strings.Join(lines, "\n") } +// confirmationReplyLanguage keeps clarification and confirmation copy on the +// same language path so default settings can drive UI text when the current +// input is too ambiguous to infer reliably. +func (s *Service) confirmationReplyLanguage(snapshot taskcontext.TaskContextSnapshot) string { + preferredInput := firstNonEmptyString(snapshot.Text, snapshot.ErrorText) + if preferredInput == "" { + preferredInput = firstNonEmptyString(snapshot.SelectionText, memoryQueryFromSnapshot(snapshot)) + } + if preferredInput != "" { + currentLanguage := languagepolicy.PreferredReplyLanguage(preferredInput) + if shouldPreferRememberedSessionLanguage(snapshot, currentLanguage) { + return strings.TrimSpace(snapshot.SessionReplyLanguage) + } + return currentLanguage + } + if rememberedLanguage := strings.TrimSpace(snapshot.SessionReplyLanguage); rememberedLanguage != "" { + return rememberedLanguage + } + return settingsDefaultReplyLanguage(s.runEngine.Settings()) +} + +func taskConfirmQuestionPromptInstructions(replyLanguage string) []string { + if isEnglishReplyLanguage(replyLanguage) { + return []string{ + "You write one confirmation question for CialloClaw before the task executes.", + "Return exactly one concise English question and nothing else.", + "Use the inferred operation and the most specific target object you can justify from the intent payload and task context.", + "Do not mention internal labels such as intent, task title, current intent, JSON, payload, or field names.", + "Do not explain your reasoning. Do not add bullets, quotes, or multiple options.", + "If the task changes files, pages, or other state, make that action explicit in the question.", + } + } + return []string{ + "You write one confirmation question for CialloClaw before the task executes.", + "Return exactly one concise Chinese question and nothing else.", + "Use the inferred operation and the most specific target object you can justify from the intent payload and task context.", + "Do not mention internal labels such as intent, task title, current intent, JSON, payload, or field names.", + "Do not explain your reasoning. Do not add bullets, quotes, or multiple options.", + "If the task changes files, pages, or other state, make that action explicit in the question.", + } +} + +func settingsDefaultReplyLanguage(settings map[string]any) string { + general := cloneMap(mapValue(normalizeSettingsSnapshot(settings), "general")) + switch strings.TrimSpace(stringValue(general, "language", "")) { + case languagepolicy.ReplyLanguageEnglish: + return languagepolicy.ReplyLanguageEnglish + case languagepolicy.ReplyLanguageChinese: + return languagepolicy.ReplyLanguageChinese + default: + return languagepolicy.ReplyLanguageChinese + } +} + func taskConfirmQuestionContextSummary(snapshot taskcontext.TaskContextSnapshot) string { parts := []string{ fmt.Sprintf("input_type=%s", snapshot.InputType), From f66e74a374d9e040fbcde9c1de0e90690c620422 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Wed, 13 May 2026 16:01:49 +0000 Subject: [PATCH 2/4] fix(orchestrator): restore confirmation language fallback Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: jinyu918 <183728970+jinyu918@users.noreply.github.com> --- .../internal/orchestrator/memory_handoffs.go | 5 +---- .../orchestrator/task_confirm_language_test.go | 16 ++++++++++++++++ .../internal/orchestrator/task_confirm_prompt.go | 12 ++++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/services/local-service/internal/orchestrator/memory_handoffs.go b/services/local-service/internal/orchestrator/memory_handoffs.go index de077bb2..5f7b383a 100644 --- a/services/local-service/internal/orchestrator/memory_handoffs.go +++ b/services/local-service/internal/orchestrator/memory_handoffs.go @@ -320,10 +320,7 @@ func clarificationBaseTextForLanguage(suggestionIntent map[string]any, replyLang func initialClarificationPromptForLanguage(snapshot taskcontext.TaskContextSnapshot, startFlow bool, replyLanguage string) string { if strings.TrimSpace(replyLanguage) == "" { - preferredInput := firstNonEmptyString(snapshot.Text, snapshot.ErrorText) - if preferredInput == "" { - preferredInput = firstNonEmptyString(snapshot.SelectionText, memoryQueryFromSnapshot(snapshot)) - } + preferredInput := clarificationLanguageEvidence(snapshot) replyLanguage = languagepolicy.PreferredReplyLanguage(preferredInput) } if replyLanguage == languagepolicy.ReplyLanguageEnglish { diff --git a/services/local-service/internal/orchestrator/task_confirm_language_test.go b/services/local-service/internal/orchestrator/task_confirm_language_test.go index 81bac08e..74735757 100644 --- a/services/local-service/internal/orchestrator/task_confirm_language_test.go +++ b/services/local-service/internal/orchestrator/task_confirm_language_test.go @@ -31,6 +31,22 @@ func TestBubbleTextForConfirmationUsesSettingsDefaultLanguage(t *testing.T) { } } +func TestBubbleTextForConfirmationUsesChineseDefaultLanguageWithoutInputSignal(t *testing.T) { + service, _ := newTestServiceWithModelClient(t, stubModelClient{ + output: `{"intent":{"name":"summarize","arguments":{"style":"brief"}}}`, + }) + + text := service.bubbleTextForConfirmation( + runengine.TaskRecord{}, + taskcontext.TaskContextSnapshot{}, + intent.Suggestion{RequiresConfirm: true}, + true, + ) + if !strings.Contains(text, "请先确认") { + t.Fatalf("expected chinese clarification bubble from default settings fallback, got %q", text) + } +} + func TestBubbleTextForConfirmationPrefersRememberedSessionLanguage(t *testing.T) { service, _ := newTestServiceWithModelClient(t, stubModelClient{ output: `{"intent":{"name":"summarize","arguments":{"style":"brief"}}}`, diff --git a/services/local-service/internal/orchestrator/task_confirm_prompt.go b/services/local-service/internal/orchestrator/task_confirm_prompt.go index ad694493..4dbbef41 100644 --- a/services/local-service/internal/orchestrator/task_confirm_prompt.go +++ b/services/local-service/internal/orchestrator/task_confirm_prompt.go @@ -159,14 +159,18 @@ func buildTaskConfirmQuestionPrompt(snapshot taskcontext.TaskContextSnapshot, su return strings.Join(lines, "\n") } +func clarificationLanguageEvidence(snapshot taskcontext.TaskContextSnapshot) string { + return firstNonEmptyString( + firstNonEmptyString(snapshot.Text, snapshot.ErrorText), + snapshot.SelectionText, + ) +} + // confirmationReplyLanguage keeps clarification and confirmation copy on the // same language path so default settings can drive UI text when the current // input is too ambiguous to infer reliably. func (s *Service) confirmationReplyLanguage(snapshot taskcontext.TaskContextSnapshot) string { - preferredInput := firstNonEmptyString(snapshot.Text, snapshot.ErrorText) - if preferredInput == "" { - preferredInput = firstNonEmptyString(snapshot.SelectionText, memoryQueryFromSnapshot(snapshot)) - } + preferredInput := clarificationLanguageEvidence(snapshot) if preferredInput != "" { currentLanguage := languagepolicy.PreferredReplyLanguage(preferredInput) if shouldPreferRememberedSessionLanguage(snapshot, currentLanguage) { From 635fbf98b33b0575aa3ea39e4c287dd443e76bcb Mon Sep 17 00:00:00 2001 From: xgopilot Date: Wed, 13 May 2026 16:26:29 +0000 Subject: [PATCH 3/4] fix(orchestrator): correct memory handoff comments Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: jinyu918 <183728970+jinyu918@users.noreply.github.com> --- .../internal/orchestrator/memory_handoffs.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/local-service/internal/orchestrator/memory_handoffs.go b/services/local-service/internal/orchestrator/memory_handoffs.go index 5f7b383a..cf8f5f00 100644 --- a/services/local-service/internal/orchestrator/memory_handoffs.go +++ b/services/local-service/internal/orchestrator/memory_handoffs.go @@ -200,8 +200,8 @@ func (s *Service) attachMemoryReadPlans(taskID, runID string, snapshot taskconte } // previewMemoryContext performs the same storage-backed retrieval as execution -// planning, but it only returns the matched summaries so clarification bubbles -// can acknowledge recent context without changing task ownership decisions. +// planning, but it only returns retrieval hits for downstream reuse in the +// clarification and confirmation flow without changing task ownership decisions. func (s *Service) previewMemoryContext(taskID, runID string, snapshot taskcontext.TaskContextSnapshot) []memory.RetrievalHit { if s == nil || s.memory == nil { return nil @@ -221,8 +221,8 @@ func (s *Service) previewMemoryContext(taskID, runID string, snapshot taskcontex } // clarificationPreviewHits prefers the retrieval_context already materialized on -// the task so clarification bubbles and later execution reuse the same memory -// evidence even after storage round-trips or follow-up confirmation RPCs. +// the task so clarification, confirmation, and later execution reuse the same +// memory evidence even after storage round-trips or follow-up confirmation RPCs. func (s *Service) clarificationPreviewHits(task runengine.TaskRecord, snapshot taskcontext.TaskContextSnapshot) []memory.RetrievalHit { if s == nil { return nil From 1c485d71d8621618c63098069c0d1fc14331d2cb Mon Sep 17 00:00:00 2001 From: xgopilot Date: Wed, 13 May 2026 17:01:22 +0000 Subject: [PATCH 4/4] fix(orchestrator): restore confirmation bubble localization fallback Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: jinyu918 <183728970+jinyu918@users.noreply.github.com> --- .../task_confirm_language_test.go | 36 ++++++++++++ .../orchestrator/task_confirm_prompt.go | 56 +++++++++++++++++-- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/services/local-service/internal/orchestrator/task_confirm_language_test.go b/services/local-service/internal/orchestrator/task_confirm_language_test.go index 74735757..f8ff9457 100644 --- a/services/local-service/internal/orchestrator/task_confirm_language_test.go +++ b/services/local-service/internal/orchestrator/task_confirm_language_test.go @@ -73,6 +73,42 @@ func TestBubbleTextForConfirmationPrefersRememberedSessionLanguage(t *testing.T) } } +func TestBubbleTextForConfirmationUsesEnglishPageContextWithoutTextSignal(t *testing.T) { + service, _ := newTestServiceWithModelClient(t, stubModelClient{ + output: `{"intent":{"name":"summarize","arguments":{"style":"brief"}}}`, + }) + + text := service.bubbleTextForConfirmation( + runengine.TaskRecord{}, + taskcontext.TaskContextSnapshot{ + PageTitle: "Release notes dashboard", + }, + intent.Suggestion{RequiresConfirm: true}, + true, + ) + if !strings.Contains(text, "Please confirm the goal first.") { + t.Fatalf("expected english clarification bubble from page context, got %q", text) + } +} + +func TestFallbackConfirmIntentTextUsesEnglishAction(t *testing.T) { + text := fallbackConfirmIntentText( + taskcontext.TaskContextSnapshot{}, + intent.Suggestion{ + Intent: map[string]any{ + "name": "summarize", + }, + }, + languagepolicy.ReplyLanguageEnglish, + ) + if strings.Contains(text, "总结") || strings.Contains(text, "处理") { + t.Fatalf("expected english fallback confirmation action, got %q", text) + } + if text != "Do you want me to summarize?" { + t.Fatalf("expected english fallback confirmation question, got %q", text) + } +} + func TestBuildTaskConfirmQuestionPromptUsesReplyLanguage(t *testing.T) { snapshot := taskcontext.TaskContextSnapshot{Text: "openclaw docs"} suggestion := intent.Suggestion{ diff --git a/services/local-service/internal/orchestrator/task_confirm_prompt.go b/services/local-service/internal/orchestrator/task_confirm_prompt.go index 4dbbef41..8fe7f58b 100644 --- a/services/local-service/internal/orchestrator/task_confirm_prompt.go +++ b/services/local-service/internal/orchestrator/task_confirm_prompt.go @@ -102,7 +102,7 @@ func shouldUseModelBackedConfirmIntentText(suggestion intent.Suggestion) bool { func fallbackConfirmIntentText(snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion, replyLanguage string) string { action, subject := intentConfirmTitleParts(suggestion.TaskTitle) if subject == "" { - action = intentConfirmAction(snapshot, suggestion.Intent) + action = intentConfirmAction(snapshot, suggestion.Intent, replyLanguage) subject = intentConfirmSubject(snapshot, suggestion.Intent) } @@ -125,11 +125,14 @@ func fallbackConfirmIntentText(snapshot taskcontext.TaskContextSnapshot, suggest } } -func intentConfirmAction(snapshot taskcontext.TaskContextSnapshot, taskIntent map[string]any) string { +func intentConfirmAction(snapshot taskcontext.TaskContextSnapshot, taskIntent map[string]any, replyLanguage string) string { intentName := strings.TrimSpace(stringValue(taskIntent, "name", "")) if intentName == "" { return "" } + if isEnglishReplyLanguage(replyLanguage) { + return englishIntentConfirmAction(snapshot, intentName) + } const subjectMarker = "__confirm_subject__" title := presentation.TaskTitle(intentName, presentation.TaskTitleOptions{ Subject: subjectMarker, @@ -143,6 +146,29 @@ func intentConfirmAction(snapshot taskcontext.TaskContextSnapshot, taskIntent ma return strings.TrimSpace(strings.TrimRight(prefix, "::")) } +func englishIntentConfirmAction(snapshot taskcontext.TaskContextSnapshot, intentName string) string { + switch intentName { + case "rewrite": + return "rewrite" + case "translate": + return "translate" + case "explain": + if strings.TrimSpace(snapshot.ErrorText) != "" { + return "explain" + } + return "explain" + case "summarize": + return "summarize" + case "screen_analyze": + if strings.TrimSpace(snapshot.ErrorText) != "" { + return "analyze" + } + return "analyze" + default: + return "handle" + } +} + func buildTaskConfirmQuestionPrompt(snapshot taskcontext.TaskContextSnapshot, suggestion intent.Suggestion, replyLanguage string) string { intentPayload, _ := json.Marshal(suggestion.Intent) lines := taskConfirmQuestionPromptInstructions(replyLanguage) @@ -160,10 +186,30 @@ func buildTaskConfirmQuestionPrompt(snapshot taskcontext.TaskContextSnapshot, su } func clarificationLanguageEvidence(snapshot taskcontext.TaskContextSnapshot) string { - return firstNonEmptyString( - firstNonEmptyString(snapshot.Text, snapshot.ErrorText), + for _, value := range []string{ + snapshot.Text, + snapshot.ErrorText, snapshot.SelectionText, - ) + firstSnapshotFile(snapshot), + snapshot.VisibleText, + snapshot.ScreenSummary, + snapshot.PageTitle, + snapshot.WindowTitle, + snapshot.ClipboardText, + snapshot.HoverTarget, + } { + if strings.TrimSpace(value) != "" { + return value + } + } + return "" +} + +func firstSnapshotFile(snapshot taskcontext.TaskContextSnapshot) string { + if len(snapshot.Files) == 0 { + return "" + } + return snapshot.Files[0] } // confirmationReplyLanguage keeps clarification and confirmation copy on the