diff --git a/source/cydo/runtime/config/package.d b/source/cydo/runtime/config/package.d index d2c21393..3d7a35d5 100644 --- a/source/cydo/runtime/config/package.d +++ b/source/cydo/runtime/config/package.d @@ -112,6 +112,10 @@ struct CydoConfig @Optional string default_agent; @Optional string default_task_type; @Optional AgentConfig[string] agents; + /// Generate next-message suggestions (a small-model one-shot at each turn + /// end and on opening a live task). Set false to stop the backend from + /// creating them at all; title generation is separate and unaffected. + @Optional SetInfo!bool suggestions; @Optional bool dev_mode; @Optional string log_level = "info"; @Optional string system_keyword = "SYSTEM"; diff --git a/source/cydo/server/app.d b/source/cydo/server/app.d index c127f5ba..ba74b838 100644 --- a/source/cydo/server/app.d +++ b/source/cydo/server/app.d @@ -547,6 +547,7 @@ class App )); derivedTextJobs = new DerivedTextJobs(DerivedTextJobsHost( getTask: (int tid) => tid in tasks ? &tasks[tid] : null, + suggestionsEnabled: () => !config.suggestions.set || config.suggestions.value, snapshotTaskIds: &snapshotTaskIdsForResume, agentForTask: &agentForTask, hasSubscribers: (int tid) => clientHub.hasSubscribers(tid), diff --git a/source/cydo/workflow/tasks/derived_text.d b/source/cydo/workflow/tasks/derived_text.d index d9c4a7d6..217d4161 100644 --- a/source/cydo/workflow/tasks/derived_text.d +++ b/source/cydo/workflow/tasks/derived_text.d @@ -16,6 +16,10 @@ import cydo.workflow.history.abbrev : buildAbbreviatedHistoryFromStrings; struct DerivedTextJobsHost { TaskData* delegate(int tid) getTask; + /// Whether suggestion generation is enabled at all (config `suggestions`); + /// null means enabled. Gating here stops the backend one-shots themselves, + /// not just the display. + bool delegate() suggestionsEnabled; int[] delegate() snapshotTaskIds; Agent delegate(int tid) agentForTask; bool delegate(int tid) hasSubscribers; @@ -144,6 +148,8 @@ public: void generateSuggestions(int tid) { + if (host_.suggestionsEnabled !is null && !host_.suggestionsEnabled()) + return; auto td = host_.getTask(tid); if (td is null) return; diff --git a/tests/e2e/suggestions-off.spec.ts b/tests/e2e/suggestions-off.spec.ts new file mode 100644 index 00000000..7e692cf0 --- /dev/null +++ b/tests/e2e/suggestions-off.spec.ts @@ -0,0 +1,47 @@ +import { + test, + expect, + enterSession, + sendMessage, + responseTimeout, + assistantText, +} from "./fixtures"; +import { writeTestConfig } from "./test-config"; + +// `suggestions: false` stops the backend from generating next-message +// suggestions at all: the one-shot is never issued, so the suggestion bar +// never populates. +test("suggestions: false stops backend generation", async ({ + page, + agentType, +}) => { + writeTestConfig( + "/tmp/playwright-home/.config/cydo/config.yaml", + `default_agent: ${agentType} +log_level: trace +suggestions: false +workspaces: + local: + root: /tmp/cydo-test-workspace +`, + ); + // Give the backend's inotify watcher time to reload the config. + await page.waitForTimeout(500); + + await enterSession(page); + await sendMessage(page, 'Please reply with "one"'); + await expect(assistantText(page, "one")).toBeVisible({ + timeout: responseTimeout(agentType), + }); + + // A second full turn bounds the wait without a wall-clock sleep: had the + // first turn generated suggestions, they would have rendered long before + // the second turn completes, since the generation one-shot answers from + // the same mock as the turns themselves. + await sendMessage(page, 'Please reply with "two"'); + await expect(assistantText(page, "two")).toBeVisible({ + timeout: responseTimeout(agentType), + }); + + await expect(page.locator(".btn-suggestion")).toHaveCount(0); +});