Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion src/components/FileBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,8 @@ const editorExtensions = [yaml(), oneDark];
const fileModified = computed(() => fileContent.value !== originalContent.value);

const editorAssistContext = computed<AssistContext>(() => ({
scope: "system",
scope: props.deploymentName ? "deployment" : "system",
deployment: props.deploymentName || undefined,
subject: editingFile.value?.path || editingFile.value?.name || "file",
seedContext: fileContent.value,
}));
Expand Down Expand Up @@ -1165,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);
Expand All @@ -1186,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);
Expand Down Expand Up @@ -1232,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 => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for parsing tool arguments should account for cases where step.arguments might already be an object (depending on how the store handles the API response). Additionally, scanning the entire session history can be optimized by ensuring JSON.parse isn't called if the result is already known to be irrelevant.

Suggested change
const assistWritesToOpenFile = (): number => {
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 args = typeof step.arguments === 'string' ? JSON.parse(step.arguments || "{}") : step.arguments;
const target = String(args?.path || "").replace(/^\/+/, "");
if (target && (target === open || open.endsWith("/" + target))) count++;
} catch {
// Unparseable arguments cannot name the open file.
}
}
}
return count;
};

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 () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation of reloadEditorContent is susceptible to race conditions if the user closes the modal or switches files while the fetch is in progress. Additionally, silent failure on fetch error may leave the user confused. This refactored version captures state before the async call and provides an error notification.

Suggested change
const reloadEditorContent = async () => {
const reloadEditorContent = async () => {
const file = editingFile.value;
if (!file) return;
try {
const response = await fileApi.value.getContent(file.path);
if (!showEditorModal.value || editingFile.value?.path !== file.path) return;
fileContent.value = response.data;
originalContent.value = response.data;
notifications.info("File updated", `${file.name} was reloaded with the assistant's change`);
} catch (error) {
notifications.error("Reload failed", `Could not reload ${file.name} after assistant update.`);
}
};

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, () => {
Comment on lines +1296 to 1297

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the editor modal is opened, seenAssistWrites starts at 0. If the assistant had already modified this file earlier in the session, the first interaction with the assistant will trigger a redundant reload and notification. It is better to initialize the counter when the file is opened.

Suggested change
watch(currentPath, () => {
watch(editingFile, (file) => {
if (file) seenAssistWrites.value = assistWritesToOpenFile();
}, { immediate: true });
watch(currentPath, () => {

fetchFiles();
});
Expand Down
27 changes: 24 additions & 3 deletions src/views/DeploymentDetailView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -604,9 +604,12 @@
<div v-if="activeTab === 'environment'" class="env-tab">
<div class="env-header">
<h3>Environment Variables</h3>
<button class="btn btn-sm btn-primary" @click="openAddEnvModal">
<i class="pi pi-plus" /> Add Variable
</button>
<div class="env-header-actions">
<AssistButton :context="envAssistContext" title="Ask the assistant about these variables" />
<button class="btn btn-sm btn-primary" @click="openAddEnvModal">
<i class="pi pi-plus" /> Add Variable
</button>
</div>
</div>
<div class="env-list">
<div v-if="envVars.length === 0" class="empty-env">
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -2817,6 +2821,17 @@ const serviceConfigAssistContext = computed<AssistContext>(() => ({
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<AssistContext>(() => ({
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<AssistContext>(() => ({
scope: "deployment",
deployment: route.params.name as string,
Expand Down Expand Up @@ -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);
Expand Down
Loading