Diagram editor pr (#1432)

* feat: add draw.io diagram editor integration

Embed draw.io editor via react-drawio (MIT, zero deps) for inline
editing of .drawio files. Changes auto-save to disk. Includes
inline editor in FilesView with Visual/Source toggle, dark mode
support, template picker for new files, and chat file attachment
integration.

* fix: debounce diagram autosave to prevent reload loop

* fix: ignore watcher-triggered xml prop changes to prevent reload loop

* fix: remove auto-save-to-disk, add manual save button for diagrams

Autosave writes triggered file watcher cascade that reloaded the
draw.io iframe and reset zoom. Replaced with explicit Save button
in the toolbar (floppy disk icon). Editor XML is stable on mount
and ignores watcher-triggered prop changes.

* fix: remove auto-save write from DiagramView, add save button

* fix: hide draw.io save/exit buttons in editor

* fix: also hide save-and-exit button

* fix: brighten save button styling, add saved confirmation

* fix: remove autoSaveStatus toggle on diagram save to prevent toolbar collapse

* fix: add local save confirmation state for diagram button

* fix: remount drawio iframe on theme change, persisting XML across mounts

* fix: clear persisted xml on mount to prevent leaking between files

* fix: initialize dark mode synchronously, preserve edits across theme remount

* fix: auto-focus drawio iframe on mount/theme-change for keyboard shortcuts

* fix: add diagram i18n keys to Traditional Chinese locale

* fix: restore upstream HMR host and LAN address support

* fix: load sub-agent sessions on bootstrap for sidebar visibility

Two-phase session load: first fetch root sessions (for accurate
sessionTotal), then fetch all sessions and include child sessions
(sub-agent delegations). This ensures sub-agent sessions appear
in the sidebar immediately instead of relying on the async global
session store.

* remove opencode-drawio from PR branch

* fix: atomic file writes to prevent concurrent read/write truncation

Three-layer defense against the O_TRUNC race:

1. Write side (server): replace direct writeFile with write-to-temp-
   then-rename. fs.rename is atomic on POSIX.

2. Read side (server): retry up to 3 times with 50ms backoff when
   readFile returns empty but stat reported non-zero size.

3. FilesView client: refuse to save empty draftContent when the
   original fileContent was non-empty.

* fix(dev): clean up orphaned OpenCode processes on Ctrl+C

* fix: allow empty file saves, log warning instead of blocking

Replaces the hard block on saving empty content with a console.warn.
The atomic write + read retry on the server side handle the O_TRUNC
race properly. The previous guard caused a UX regression by silently
preventing users from clearing a file and saving.

* fix: remove time window from sub-agent fallback for live tasks

While a task tool is active, the fallback now matches any session
with the correct parentID regardless of creation time. This allows
late-appearing child sessions to be found when the OpenCode server
is slow or the SSE event pipeline is delayed. The time window is
still applied once the task tool has completed, as a final sanity
check.

* fix: three diagram editor bugs from Greptile review

1. stableXmlRef now resets when xml prop changes — switching
   between .drawio files renders the correct content.

2. Focus effect only runs on mount, not on isDark changes —
   theme toggle no longer steals keyboard focus 600ms later.

3. saveDiagram updates xml state after writing — dirty-check
   guard works correctly for subsequent saves.

* fix: route session.created SSE events to correct directory

Three-layer fix for sub-agent sessions not appearing in sidebar and
inline chat:

1. protocol.js: parseSseEventEnvelope now extracts directory from
   properties.info.directory (where session.created/updated events
   carry it) in addition to properties.directory. WS frames relayed
   to the browser now carry the real directory instead of 'global',
   so child sessions routed to the correct directory store.

2. event-pipeline.ts: same fallback in resolveEventDirectory for
   defense-in-depth when SSE events bypass the WS relay.

3. resolveFallbackTaskSessionId.ts: time window lower bound now
   allows 2s grace before taskStartTime to accommodate server timing
   jitter (child session creation timestamps consistently precede the
   tool's recorded start by ~6-9ms), fixing the 'Open subtask'
   button not rendering in OpenChamber's inline chat.

* fix: sub-agent sidebar visibility, file zeroing guard, inline badge fallback

- Sync watchdog: periodic child session discovery poll (every 15s) detects
  sessions created by other OpenCode instances, triggers parent materialization
- protocol.js: parseSseEventEnvelope extracts directory from
  properties.info.directory for session.created/updated events
- event-pipeline.ts: same fallback in resolveEventDirectory for defense-in-depth
- resolveFallbackTaskSessionId: don't require taskStartTime (cross-OpenCode);
  pick most recent child when multiple idle candidates exist
- readTaskSessionIdFromOutput: parse <task id="ses_xxx"> format from output
- FilesView: reinstate empty-draft guard (block save when draftContent='' but
  fileContent had content) to prevent file zeroing on tab switch

* Fix diagram autosave reload loop

* Highlight drawio files as XML

* Use diff-compatible highlighting for drawio files

* Restore drawio file icon mapping

* Stabilize drawio source preview toggle

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
nerdosaurus
2026-06-08 18:50:56 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 9b52222ef1
commit d9b9b56599
31 changed files with 7102 additions and 6477 deletions
+4
View File
@@ -23,6 +23,7 @@ export const dict = {
'layout.mainTab.chat': 'Chat',
'layout.mainTab.plan': 'Plan',
'layout.mainTab.diff': 'Diff',
'layout.mainTab.diagram': 'Diagram',
'layout.mainTab.files': 'Files',
'layout.mainTab.terminal': 'Terminal',
'layout.mainTab.context': 'Context',
@@ -1120,6 +1121,8 @@ export const dict = {
'filesView.error.switchToEditMode': 'Switch to edit mode to fix the issue.',
'filesView.error.readFileFailed': 'Failed to read file',
'filesView.editor.htmlPreviewTitle': 'HTML Preview',
'filesView.diagram.closeDiagramView': 'Close diagram view',
'filesView.diagram.saveDiagram': 'Save diagram',
'contextUsage.aria.label': 'Context usage',
'contextUsage.mobile.title': 'Context Usage',
'contextUsage.mobile.usedTokens': 'Used tokens',
@@ -1565,6 +1568,7 @@ export const dict = {
'chat.fileAttachment.activeEditor.addFile': 'Add file:{name} to context',
'chat.fileAttachment.activeEditor.pinSelection': 'Pin selection to context',
'chat.fileAttachment.activeEditor.remove': 'Remove from context',
'chat.fileAttachment.openInDiagram': 'Open in diagram view',
'chat.pendingChanges.fileCountSingle': '{count} file',
'chat.pendingChanges.fileCountPlural': '{count} files',
'chat.pendingChanges.changedInWorkspace': 'changed in workspace',
+4
View File
@@ -24,6 +24,7 @@ export const dict: Record<I18nKey, string> = {
"layout.mainTab.chat": "Chat",
"layout.mainTab.plan": "Plan",
"layout.mainTab.diff": "Diff",
"layout.mainTab.diagram": "Diagrama",
"layout.mainTab.files": "Archivos",
"layout.mainTab.terminal": "Terminal",
"layout.mainTab.context": "Contexto",
@@ -1086,6 +1087,8 @@ export const dict: Record<I18nKey, string> = {
"filesView.error.switchToEditMode": "Cambia al modo de edición para resolver el problema.",
"filesView.error.readFileFailed": "No se pudo leer el archivo",
"filesView.editor.htmlPreviewTitle": "Vista previa HTML",
"filesView.diagram.closeDiagramView": "Cerrar vista de diagrama",
"filesView.diagram.saveDiagram": "Guardar diagrama",
"contextUsage.aria.label": "Uso del contexto",
"contextUsage.mobile.title": "Uso del contexto",
"contextUsage.mobile.usedTokens": "Tokens usados",
@@ -1531,6 +1534,7 @@ export const dict: Record<I18nKey, string> = {
"chat.fileAttachment.activeEditor.addFile": "Agregar archivo:{name} al contexto",
"chat.fileAttachment.activeEditor.pinSelection": "Anclar selección al contexto",
"chat.fileAttachment.activeEditor.remove": "Quitar del contexto",
"chat.fileAttachment.openInDiagram": "Abrir en vista de diagrama",
"chat.pendingChanges.fileCountSingle": "{count} archivo",
"chat.pendingChanges.fileCountPlural": "{count} archivos",
"chat.pendingChanges.changedInWorkspace": "modificado en el espacio de trabajo",
+4
View File
@@ -24,6 +24,7 @@ export const dict: Record<I18nKey, string> = {
'layout.mainTab.chat': '채팅',
'layout.mainTab.plan': '계획',
'layout.mainTab.diff': '변경사항',
'layout.mainTab.diagram': '다이어그램',
'layout.mainTab.files': '파일',
'layout.mainTab.terminal': '터미널',
'layout.mainTab.context': '컨텍스트',
@@ -1123,6 +1124,8 @@ export const dict: Record<I18nKey, string> = {
'filesView.error.switchToEditMode': '문제를 수정하려면 편집 모드로 전환하세요.',
'filesView.error.readFileFailed': '파일 읽기 실패',
'filesView.editor.htmlPreviewTitle': 'HTML 미리보기',
'filesView.diagram.closeDiagramView': '다이어그램 보기 닫기',
'filesView.diagram.saveDiagram': '다이어그램 저장',
'contextUsage.aria.label': '컨텍스트 사용량',
'contextUsage.mobile.title': '컨텍스트 사용량',
'contextUsage.mobile.usedTokens': '사용한 토큰',
@@ -1567,6 +1570,7 @@ export const dict: Record<I18nKey, string> = {
'chat.fileAttachment.activeEditor.addFile': '컨텍스트에 파일 추가:{name}',
'chat.fileAttachment.activeEditor.pinSelection': '컨텍스트에 선택 고정',
'chat.fileAttachment.activeEditor.remove': '컨텍스트에서 제거',
'chat.fileAttachment.openInDiagram': '다이어그램 보기에서 열기',
'chat.pendingChanges.fileCountSingle': '{count} 파일',
'chat.pendingChanges.fileCountPlural': '{count} 파일',
'chat.pendingChanges.changedInWorkspace': '워크스페이스에서 변경됨',
+4
View File
@@ -25,6 +25,7 @@ export const dict: Record<I18nKey, string> = {
'layout.mainTab.chat': 'Czat',
'layout.mainTab.plan': 'Plan',
'layout.mainTab.diff': 'Różnice',
'layout.mainTab.diagram': 'Diagram',
'layout.mainTab.files': 'Pliki',
'layout.mainTab.terminal': 'Terminal',
'layout.mainTab.context': 'Kontekst',
@@ -541,6 +542,7 @@ export const dict: Record<I18nKey, string> = {
'chat.fileAttachment.activeEditor.addFile': 'Dodaj plik:{name} do kontekstu',
'chat.fileAttachment.activeEditor.pinSelection': 'Przypnij zaznaczenie do kontekstu',
'chat.fileAttachment.activeEditor.remove': 'Usuń z kontekstu',
'chat.fileAttachment.openInDiagram': 'Otwórz w widoku diagramu',
'chat.pendingChanges.fileCountSingle': '{count} plik',
'chat.pendingChanges.fileCountPlural': '{count} plików',
'chat.pendingChanges.changedInWorkspace': 'zmienione w przestrzeni roboczej',
@@ -1499,6 +1501,8 @@ export const dict: Record<I18nKey, string> = {
'filesView.editor.fullscreen': 'Pełny ekran',
'filesView.editor.goToLine': 'Przejdź do linii',
'filesView.editor.htmlPreviewTitle': 'Podgląd HTML',
'filesView.diagram.closeDiagramView': 'Zamknij widok diagramu',
'filesView.diagram.saveDiagram': 'Zapisz diagram',
'filesView.editor.imageAltFallback': 'Obraz',
'filesView.editor.openFilesAria': 'Otwarte pliki',
'filesView.editor.openInDesktopApp': 'Otwórz w aplikacji desktopowej',
@@ -24,6 +24,7 @@ export const dict: Record<I18nKey, string> = {
"layout.mainTab.chat": "Chat",
"layout.mainTab.plan": "Plano",
"layout.mainTab.diff": "Diff",
"layout.mainTab.diagram": "Diagrama",
"layout.mainTab.files": "Arquivos",
"layout.mainTab.terminal": "Terminal",
"layout.mainTab.context": "Contexto",
@@ -1086,6 +1087,8 @@ export const dict: Record<I18nKey, string> = {
"filesView.error.switchToEditMode": "Alterne para o modo de edição para resolver o problema.",
"filesView.error.readFileFailed": "Não foi possível ler o arquivo",
"filesView.editor.htmlPreviewTitle": "Pré-visualização HTML",
"filesView.diagram.closeDiagramView": "Fechar visualização de diagrama",
"filesView.diagram.saveDiagram": "Salvar diagrama",
"contextUsage.aria.label": "Uso do contexto",
"contextUsage.mobile.title": "Uso do contexto",
"contextUsage.mobile.usedTokens": "Tokens usados",
@@ -1531,6 +1534,7 @@ export const dict: Record<I18nKey, string> = {
"chat.fileAttachment.activeEditor.addFile": "Adicionar arquivo:{name} ao contexto",
"chat.fileAttachment.activeEditor.pinSelection": "Fixar seleção no contexto",
"chat.fileAttachment.activeEditor.remove": "Remover do contexto",
"chat.fileAttachment.openInDiagram": "Abrir na visualização de diagrama",
"chat.pendingChanges.fileCountSingle": "{count} arquivo",
"chat.pendingChanges.fileCountPlural": "{count} arquivos",
"chat.pendingChanges.changedInWorkspace": "modificado no workspace",
+4
View File
@@ -24,6 +24,7 @@ export const dict: Record<I18nKey, string> = {
"layout.mainTab.chat": "Чат",
"layout.mainTab.plan": "План",
"layout.mainTab.diff": "Diff",
"layout.mainTab.diagram": "Діаграма",
"layout.mainTab.files": "Файли",
"layout.mainTab.terminal": "Термінал",
"layout.mainTab.context": "Контекст",
@@ -1086,6 +1087,8 @@ export const dict: Record<I18nKey, string> = {
"filesView.error.switchToEditMode": "Перейдіть у режим редагування, щоб усунути проблему.",
"filesView.error.readFileFailed": "Не вдалося прочитати файл",
"filesView.editor.htmlPreviewTitle": "Попередній перегляд HTML",
"filesView.diagram.closeDiagramView": "Закрити перегляд діаграми",
"filesView.diagram.saveDiagram": "Зберегти діаграму",
"contextUsage.aria.label": "Використання контексту",
"contextUsage.mobile.title": "Використання контексту",
"contextUsage.mobile.usedTokens": "Використані токени",
@@ -1531,6 +1534,7 @@ export const dict: Record<I18nKey, string> = {
"chat.fileAttachment.activeEditor.addFile": "Додати файл:{name} до контексту",
"chat.fileAttachment.activeEditor.pinSelection": "Закріпити вибір у контексті",
"chat.fileAttachment.activeEditor.remove": "Видалити з контексту",
"chat.fileAttachment.openInDiagram": "Відкрити в перегляді діаграми",
"chat.pendingChanges.fileCountSingle": "Файл: {count}",
"chat.pendingChanges.fileCountPlural": "Файлів: {count}",
"chat.pendingChanges.changedInWorkspace": "змінено в гілці",
@@ -24,6 +24,7 @@ export const dict: Record<I18nKey, string> = {
'layout.mainTab.chat': '聊天',
'layout.mainTab.plan': '计划',
'layout.mainTab.diff': '差异',
'layout.mainTab.diagram': '图表',
'layout.mainTab.files': '文件',
'layout.mainTab.terminal': '终端',
'layout.mainTab.context': '上下文',
@@ -1086,6 +1087,8 @@ export const dict: Record<I18nKey, string> = {
'filesView.error.switchToEditMode': '请切换到编辑模式修复问题。',
'filesView.error.readFileFailed': '读取文件失败',
'filesView.editor.htmlPreviewTitle': 'HTML 预览',
'filesView.diagram.closeDiagramView': '关闭图表视图',
'filesView.diagram.saveDiagram': '保存图表',
'contextUsage.aria.label': '上下文用量',
'contextUsage.mobile.title': '上下文用量',
'contextUsage.mobile.usedTokens': '已用 Token',
@@ -1531,6 +1534,7 @@ export const dict: Record<I18nKey, string> = {
'chat.fileAttachment.activeEditor.addFile': '将文件添加到上下文:{name}',
'chat.fileAttachment.activeEditor.pinSelection': '将选择固定到上下文',
'chat.fileAttachment.activeEditor.remove': '从上下文中移除',
'chat.fileAttachment.openInDiagram': '在图表视图中打开',
'chat.pendingChanges.fileCountSingle': '{count} 个文件',
'chat.pendingChanges.fileCountPlural': '{count} 个文件',
'chat.pendingChanges.changedInWorkspace': '工作区中有变更',
@@ -24,6 +24,7 @@ export const dict: Record<I18nKey, string> = {
'layout.mainTab.chat': '聊天',
'layout.mainTab.plan': '計畫',
'layout.mainTab.diff': '差異',
'layout.mainTab.diagram': '圖表',
'layout.mainTab.files': '檔案',
'layout.mainTab.terminal': '終端機',
'layout.mainTab.context': '上下文',
@@ -1096,6 +1097,8 @@ export const dict: Record<I18nKey, string> = {
'filesView.error.switchToEditMode': '請切換到編輯模式修復問題。',
'filesView.error.readFileFailed': '讀取檔案失敗',
'filesView.editor.htmlPreviewTitle': 'HTML 預覽',
'filesView.diagram.closeDiagramView': '關閉圖表檢視',
'filesView.diagram.saveDiagram': '儲存圖表',
'contextUsage.aria.label': '上下文用量',
'contextUsage.mobile.title': '上下文用量',
'contextUsage.mobile.usedTokens': '已用 Token',
@@ -1535,6 +1538,7 @@ export const dict: Record<I18nKey, string> = {
'chat.fileAttachment.activeEditor.addFile': '將檔案加入上下文:{name}',
'chat.fileAttachment.activeEditor.pinSelection': '將選擇釘選到上下文',
'chat.fileAttachment.activeEditor.remove': '從上下文中移除',
'chat.fileAttachment.openInDiagram': '在圖表檢視中開啟',
'chat.pendingChanges.fileCountSingle': '{count} 個檔案',
'chat.pendingChanges.fileCountPlural': '{count} 個檔案',
'chat.pendingChanges.changedInWorkspace': '工作區中有變更',