feat: Enable file editing and saving (#252)

## Summary
- Adds file writing capability to Monaco editor in the file viewer
- Implements writeFile API on the server for workspace files
- Integrates save functionality into the file viewer UI with proper
state management

## Bug Fixes (Review Feedback)
- Fixed failed save discarding edits when switching files - now checks
save result and only proceeds if successful
- Fixed refresh overwriting dirty editor state - now prompts for
confirmation before discarding edits
- Fixed save button unable to save empty files - changed check from `if
(content)` to `if (content !== undefined && content !== null)`
- Added agent edit conflict detection - when agent edits file while user
has unsaved changes, shows conflict dialog with Overwrite/Cancel options
- Fixed dialog appearing behind unpinned sidebar - increased alert
dialog z-index to z-100

## Related Issues
- Closes #251

---------

Co-authored-by: Jess Chadwick <jchadwick@gmail.com>
This commit is contained in:
Jess Chadwick
2026-03-29 17:41:11 -04:00
committed by GitHub
parent 55a6479c0e
commit 37b3f85e61
22 changed files with 324 additions and 19 deletions

View File

@@ -234,6 +234,16 @@ export const serverApi = {
`/api/workspaces/${encodeURIComponent(id)}/files/content?${params.toString()}`,
)
},
writeWorkspaceFile(id: string, relativePath: string, contents: string): Promise<void> {
const params = new URLSearchParams({ path: relativePath })
return request(
`/api/workspaces/${encodeURIComponent(id)}/files/content?${params.toString()}`,
{
method: "PUT",
body: JSON.stringify({ contents }),
},
)
},
fetchConfigOwner<T extends Record<string, any> = Record<string, any>>(owner: string): Promise<T> {
return request<T>(`/api/storage/config/${encodeURIComponent(owner)}`)

View File

@@ -95,6 +95,18 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "Status",
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
"instanceShell.rightPanel.actions.refresh": "Refresh",
"instanceShell.rightPanel.actions.save": "Save (Ctrl+S)",
"instanceShell.rightPanel.actions.saveConfirm.message": "Do you want to save changes to \"{path}\" before switching?",
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Save",
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Discard Changes",
"instanceShell.rightPanel.actions.conflict.message": "File was modified by the agent. Overwrite agent's changes?",
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Overwrite",
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Cancel",
"instanceShell.rightPanel.actions.refreshDirty.message": "File has unsaved changes. Refresh will discard your edits. Continue?",
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Refresh",
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancel",
"instanceShell.rightPanel.toast.saveSuccess": "File saved successfully",
"instanceShell.rightPanel.toast.saveError": "Failed to save file",
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
"instanceShell.rightPanel.sections.plan": "Plan",

View File

@@ -94,6 +94,19 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.files": "Archivos",
"instanceShell.rightPanel.tabs.status": "Estado",
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
"instanceShell.rightPanel.actions.refresh": "Actualizar",
"instanceShell.rightPanel.actions.save": "Guardar (Ctrl+S)",
"instanceShell.rightPanel.actions.saveConfirm.message": "¿Deseas guardar los cambios en \"{path}\" antes de cambiar?",
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Guardar",
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Descartar cambios",
"instanceShell.rightPanel.actions.conflict.message": "El archivo fue modificado por el agente. ¿Sobrescribir los cambios del agente?",
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Sobrescribir",
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Cancelar",
"instanceShell.rightPanel.actions.refreshDirty.message": "El archivo tiene cambios sin guardar. Actualizar discardará tus ediciones. ¿Continuar?",
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Actualizar",
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancelar",
"instanceShell.rightPanel.toast.saveSuccess": "Archivo guardado exitosamente",
"instanceShell.rightPanel.toast.saveError": "Error al guardar el archivo",
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesión",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
"instanceShell.rightPanel.sections.plan": "Plan",

View File

@@ -94,6 +94,19 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.files": "Fichiers",
"instanceShell.rightPanel.tabs.status": "Statut",
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
"instanceShell.rightPanel.actions.refresh": "Actualiser",
"instanceShell.rightPanel.actions.save": "Enregistrer (Ctrl+S)",
"instanceShell.rightPanel.actions.saveConfirm.message": "Voulez-vous enregistrer les modifications de \"{path}\" avant de changer ?",
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Enregistrer",
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Annuler les modifications",
"instanceShell.rightPanel.actions.conflict.message": "Le fichier a été modifié par l'agent. Écraser les modifications de l'agent ?",
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Écraser",
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Annuler",
"instanceShell.rightPanel.actions.refreshDirty.message": "Le fichier a des modifications non enregistrées. Actualiser supprimera vos modifications. Continuer ?",
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Actualiser",
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Annuler",
"instanceShell.rightPanel.toast.saveSuccess": "Fichier enregistré avec succès",
"instanceShell.rightPanel.toast.saveError": "Échec de l'enregistrement du fichier",
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
"instanceShell.rightPanel.sections.plan": "Plan",

View File

@@ -95,6 +95,18 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.status": "סטטוס",
"instanceShell.rightPanel.tabs.ariaLabel": "לשוניות לוח ימני",
"instanceShell.rightPanel.actions.refresh": "רענן",
"instanceShell.rightPanel.actions.save": "שמור (Ctrl+S)",
"instanceShell.rightPanel.actions.saveConfirm.message": "האם ברצונך לשמור את השינויים לפני המעבר?",
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "שמור",
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "בטל שינויים",
"instanceShell.rightPanel.actions.conflict.message": "הקובץ שונה על ידי הסוכן. לדרוס את שינויי הסוכן?",
"instanceShell.rightPanel.actions.conflict.confirmLabel": "דרוס",
"instanceShell.rightPanel.actions.conflict.cancelLabel": "בטל",
"instanceShell.rightPanel.actions.refreshDirty.message": "לקובץ יש שינויים שלא נשמרו. רענון יבטל את העריכות שלך. להמשיך?",
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "רענן",
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "בטל",
"instanceShell.rightPanel.toast.saveSuccess": "הקובץ נשמר בהצלחה",
"instanceShell.rightPanel.toast.saveError": "כשלון בשמירת הקובץ",
"instanceShell.rightPanel.sections.sessionChanges": "שינויי סשן",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "קבצים שהשתנו בסשן הנוכחי. מציג הוספות ומחיקות לכל קובץ.",
"instanceShell.rightPanel.sections.plan": "תוכנית",

View File

@@ -94,6 +94,19 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.files": "ファイル",
"instanceShell.rightPanel.tabs.status": "ステータス",
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
"instanceShell.rightPanel.actions.refresh": "更新",
"instanceShell.rightPanel.actions.save": "保存 (Ctrl+S)",
"instanceShell.rightPanel.actions.saveConfirm.message": "「{path}」への変更を切り替え前に保存しますか?",
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "保存",
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "変更を破棄",
"instanceShell.rightPanel.actions.conflict.message": "ファイルはエージェントによって変更されました。上書きしますか?",
"instanceShell.rightPanel.actions.conflict.confirmLabel": "上書き",
"instanceShell.rightPanel.actions.conflict.cancelLabel": "キャンセル",
"instanceShell.rightPanel.actions.refreshDirty.message": "ファイルには未保存の変更があります。更新すると編集が破棄されます。続行しますか?",
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "更新",
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "キャンセル",
"instanceShell.rightPanel.toast.saveSuccess": "ファイルを保存しました",
"instanceShell.rightPanel.toast.saveError": "ファイルの保存に失敗しました",
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
"instanceShell.rightPanel.sections.plan": "計画",

View File

@@ -94,6 +94,19 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.files": "Файлы",
"instanceShell.rightPanel.tabs.status": "Статус",
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
"instanceShell.rightPanel.actions.refresh": "Обновить",
"instanceShell.rightPanel.actions.save": "Сохранить (Ctrl+S)",
"instanceShell.rightPanel.actions.saveConfirm.message": "Сохранить изменения в \"{path}\" перед переключением?",
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "Сохранить",
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "Отменить изменения",
"instanceShell.rightPanel.actions.conflict.message": "Файл был изменён агентом. Перезаписать изменения агента?",
"instanceShell.rightPanel.actions.conflict.confirmLabel": "Перезаписать",
"instanceShell.rightPanel.actions.conflict.cancelLabel": "Отмена",
"instanceShell.rightPanel.actions.refreshDirty.message": "Файл имеет несохранённые изменения. Обновление отменит ваши правки. Продолжить?",
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "Обновить",
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Отмена",
"instanceShell.rightPanel.toast.saveSuccess": "Файл успешно сохранён",
"instanceShell.rightPanel.toast.saveError": "Не удалось сохранить файл",
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
"instanceShell.rightPanel.sections.plan": "План",

View File

@@ -94,6 +94,19 @@ export const instanceMessages = {
"instanceShell.rightPanel.tabs.files": "文件",
"instanceShell.rightPanel.tabs.status": "状态",
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
"instanceShell.rightPanel.actions.refresh": "刷新",
"instanceShell.rightPanel.actions.save": "保存 (Ctrl+S)",
"instanceShell.rightPanel.actions.saveConfirm.message": "切换前是否保存对 \"{path}\" 的更改?",
"instanceShell.rightPanel.actions.saveConfirm.confirmLabel": "保存",
"instanceShell.rightPanel.actions.saveConfirm.cancelLabel": "放弃更改",
"instanceShell.rightPanel.actions.conflict.message": "文件已被代理修改。是否覆盖代理的更改?",
"instanceShell.rightPanel.actions.conflict.confirmLabel": "覆盖",
"instanceShell.rightPanel.actions.conflict.cancelLabel": "取消",
"instanceShell.rightPanel.actions.refreshDirty.message": "文件有未保存的更改。刷新将放弃您的编辑。继续?",
"instanceShell.rightPanel.actions.refreshDirty.confirmLabel": "刷新",
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "取消",
"instanceShell.rightPanel.toast.saveSuccess": "文件保存成功",
"instanceShell.rightPanel.toast.saveError": "保存文件失败",
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
"instanceShell.rightPanel.sections.plan": "计划",