From bfb0297fc45f783d50b9e36c428ff8759275bbe1 Mon Sep 17 00:00:00 2001 From: nfebe Date: Wed, 22 Jul 2026 09:08:28 +0100 Subject: [PATCH 1/2] feat(ai): Scope file-editor assistant to the deployment and cover env vars Asking the assistant from a deployment's file editor now opens a deployment-scoped session, so it can use that deployment's tools and offer one-click actions instead of answering as the whole system. The environment tab gains an assistant button that shares only variable names, never values, since an unsaved draft value cannot be redacted server side. --- src/components/FileBrowser.vue | 3 ++- src/views/DeploymentDetailView.vue | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/components/FileBrowser.vue b/src/components/FileBrowser.vue index 1e5b853..9cd3f47 100644 --- a/src/components/FileBrowser.vue +++ b/src/components/FileBrowser.vue @@ -745,7 +745,8 @@ const editorExtensions = [yaml(), oneDark]; const fileModified = computed(() => fileContent.value !== originalContent.value); const editorAssistContext = computed(() => ({ - scope: "system", + scope: props.deploymentName ? "deployment" : "system", + deployment: props.deploymentName || undefined, subject: editingFile.value?.path || editingFile.value?.name || "file", seedContext: fileContent.value, })); diff --git a/src/views/DeploymentDetailView.vue b/src/views/DeploymentDetailView.vue index 8b93f01..448e6be 100755 --- a/src/views/DeploymentDetailView.vue +++ b/src/views/DeploymentDetailView.vue @@ -604,9 +604,12 @@

Environment Variables

- +
+ + +
@@ -1825,6 +1828,7 @@ import { matchTypeHints, describeBlockedRule } from "@/utils/protectedMode"; import { usePlanFlow } from "@/composables/usePlanFlow"; import SplitActionButton from "@/components/base/SplitActionButton.vue"; import SubTabs from "@/components/base/SubTabs.vue"; +import AssistButton from "@/components/ai/AssistButton.vue"; import InlineAssist from "@/components/ai/InlineAssist.vue"; import { useAssistStore } from "@/stores/assist"; import Icon from "@/components/base/Icon.vue"; @@ -2817,6 +2821,17 @@ const serviceConfigAssistContext = computed(() => ({ seedContext: serviceConfig.value, })); +// Keys only: a value typed here may not be saved yet, so the server-side +// redactor cannot know it. Withholding values keeps drafts off the model. +const envAssistContext = computed(() => ({ + scope: "deployment", + deployment: route.params.name as string, + subject: "environment variables", + seedContext: + "Environment variable keys configured for this deployment (values withheld):\n" + + (envVars.value.length ? envVars.value.map((env) => "- " + env.key).join("\n") : "(none)"), +})); + const operationAssistContext = computed(() => ({ scope: "deployment", deployment: route.params.name as string, @@ -4047,6 +4062,12 @@ onUnmounted(() => { margin-bottom: var(--space-4); } +.env-header-actions { + display: flex; + gap: var(--space-2); + align-items: center; +} + .env-header h3 { font-size: var(--text-lg); font-weight: var(--font-semibold); From ddd448013a2d698f19369656bc8d7ff44d695d4e Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 23 Jul 2026 01:56:31 +0100 Subject: [PATCH 2/2] feat(ai): Reload the open editor when the assistant writes the file An assistant write left the open editor buffer stale, so saving afterwards would silently overwrite the assistant's change. The editor now reloads the buffer when an approved write lands on the open file, and warns instead when the operator has unsaved edits. --- src/components/FileBrowser.vue | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/components/FileBrowser.vue b/src/components/FileBrowser.vue index 9cd3f47..44c1502 100644 --- a/src/components/FileBrowser.vue +++ b/src/components/FileBrowser.vue @@ -1166,6 +1166,8 @@ const viewFile = async (file: FileInfo) => { loadingFileContent.value = true; fileContent.value = ""; originalContent.value = ""; + // Writes from an earlier conversation are already in the file being loaded. + seenAssistWrites.value = assistWritesToOpenFile(); try { const response = await fileApi.value.getContent(file.path); @@ -1187,6 +1189,8 @@ const openFileEditor = async (file: FileInfo) => { loadingFileContent.value = true; fileContent.value = ""; originalContent.value = ""; + // Writes from an earlier conversation are already in the file being loaded. + seenAssistWrites.value = assistWritesToOpenFile(); try { const response = await fileApi.value.getContent(file.path); @@ -1233,6 +1237,63 @@ const saveFile = async () => { } }; +// A file write by the assistant leaves the open editor buffer stale, and a +// later manual save would clobber it. Track completed assistant writes to the +// open file and reload the buffer, unless the operator has unsaved edits. +const seenAssistWrites = ref(0); + +const assistWritesToOpenFile = (): number => { + const session = assistStore.session; + const open = (editingFile.value?.path || "").replace(/^\/+/, ""); + if (!session || !open) return 0; + let count = 0; + for (const turn of session.messages) { + for (const step of turn.tool_steps || []) { + if (step.name !== "write_deployment_file" || !step.result || step.result.startsWith("Error")) continue; + try { + const target = String(JSON.parse(step.arguments || "{}").path || "").replace(/^\/+/, ""); + if (target && (target === open || open.endsWith("/" + target))) count++; + } catch { + // Unparseable arguments cannot name the open file. + } + } + } + return count; +}; + +const reloadEditorContent = async () => { + if (!editingFile.value) return; + try { + const response = await fileApi.value.getContent(editingFile.value.path); + fileContent.value = response.data; + originalContent.value = response.data; + notifications.info("File updated", `${editingFile.value.name} was reloaded with the assistant's change`); + } catch { + // Keep the buffer; reopening the file re-reads it. + } +}; + +watch( + () => assistStore.session, + async () => { + if (!showEditorModal.value || !editingFile.value) return; + const count = assistWritesToOpenFile(); + if (count <= seenAssistWrites.value) { + seenAssistWrites.value = count; + return; + } + seenAssistWrites.value = count; + if (!viewOnly.value && fileModified.value) { + notifications.warning( + "File changed on disk", + "The assistant updated this file, but you have unsaved edits. Saving now would overwrite its change.", + ); + return; + } + await reloadEditorContent(); + }, +); + watch(currentPath, () => { fetchFiles(); });