From a008dac488235a8898ea0f979e2bacd3864c415c Mon Sep 17 00:00:00 2001 From: xcaliber Date: Sat, 28 Feb 2026 11:58:27 +0000 Subject: [PATCH] Changeset 0.17.2 (#77) --- .gitea/workflows/ci.yaml | 129 ++++- CHANGELOG.md | 65 +++ Dockerfile | 14 +- Dockerfile.frontend | 11 +- VERSION | 2 +- docs/ARCHITECTURE.md | 127 +++-- docs/DESIGN-0.17.3.md | 451 +++++++++++++++++ docs/DESIGN-CM6.md | 4 +- docs/ROADMAP.md | 60 +-- scripts/build-editor.sh | 44 ++ src/css/styles.css | 80 ++- src/editor/build.mjs | 54 +++ src/editor/chat-input.mjs | 310 ++++++++++++ src/editor/code-editor.mjs | 241 +++++++++ src/editor/index.mjs | 61 +++ src/editor/package-lock.json | 917 +++++++++++++++++++++++++++++++++++ src/editor/package.json | 28 ++ src/editor/theme.mjs | 186 +++++++ src/index.html | 37 +- src/js/admin-handlers.js | 127 ++++- src/js/attachments.js | 9 +- src/js/chat.js | 89 +++- src/js/debug.js | 5 + src/js/tokens.js | 3 +- src/js/ui-settings.js | 77 +++ src/sw.js | 3 +- 26 files changed, 3018 insertions(+), 116 deletions(-) create mode 100644 docs/DESIGN-0.17.3.md create mode 100644 scripts/build-editor.sh create mode 100644 src/editor/build.mjs create mode 100644 src/editor/chat-input.mjs create mode 100644 src/editor/code-editor.mjs create mode 100644 src/editor/index.mjs create mode 100644 src/editor/package-lock.json create mode 100644 src/editor/package.json create mode 100644 src/editor/theme.mjs diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 07cd146..f1c491e 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,15 +1,24 @@ # .gitea/workflows/ci.yaml # ============================================ -# Chat Switchboard - CI/CD Pipeline (v0.17.1) +# Chat Switchboard - CI/CD Pipeline (v0.17.2) # ============================================ # Cluster deployments use SEPARATE FE + BE images. # Unified image is for Docker Hub only (docker-compose use). # # Pipeline: -# 1a. Frontend tests (Node.js — contracts, model logic, policy wiring) -# 1b. Go test (Postgres — all PRs and pushes) -# 1c. Go test (SQLite — compilation + store verification) -# 2. Build + Deploy (depends on all test jobs passing) +# 0. Detect changes (path-based gating for all downstream jobs) +# 1a. Frontend tests — skipped if only BE/docs changed +# 1b. Go test (PG) — skipped if only FE/docs changed +# 1c. Go test (SQLite) — skipped if only FE/docs changed +# 2. Build + Deploy — skipped if docs-only change +# +# Path gating rules: +# src/, src/editor/ → frontend tests +# server/, scripts/db-* → backend tests (PG + SQLite) +# Dockerfile*, k8s/, .gitea/ → all tests (infra change) +# docs/, *.md → skip all tests + deploy +# VERSION, scripts/* → frontend + backend tests +# Tags (v*) → always full pipeline # # Deployment mapping (single domain, path-based): # PR → FE + BE :dev → switchboard.DOMAIN/dev/ (DB wipe + fresh schema) @@ -76,11 +85,99 @@ env: DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE || 'gobha/chat-switchboard' }} jobs: + # ── Stage 0: Detect Changed Paths ────────────── + # Sets output flags so downstream jobs can skip when irrelevant. + # Tags always set all flags true (full pipeline for releases). + detect-changes: + runs-on: ubuntu-latest + outputs: + frontend: ${{ steps.filter.outputs.frontend }} + backend: ${{ steps.filter.outputs.backend }} + infra: ${{ steps.filter.outputs.infra }} + docs_only: ${{ steps.filter.outputs.docs_only }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # need history for diff + + - name: Detect changed paths + id: filter + run: | + # Tags = release → run everything + if [[ "${{ gitea.ref }}" == refs/tags/v* ]]; then + echo "frontend=true" >> "$GITHUB_OUTPUT" + echo "backend=true" >> "$GITHUB_OUTPUT" + echo "infra=true" >> "$GITHUB_OUTPUT" + echo "docs_only=false" >> "$GITHUB_OUTPUT" + echo "🏷️ Tag build — running full pipeline" + exit 0 + fi + + # Determine diff base + if [[ "${{ gitea.event_name }}" == "pull_request" ]]; then + BASE="${{ gitea.event.pull_request.base.sha }}" + HEAD="${{ gitea.event.pull_request.head.sha }}" + else + # Push to main — compare with previous commit + BASE="${{ gitea.event.before }}" + HEAD="${{ gitea.sha }}" + fi + + echo "Comparing ${BASE:0:8}..${HEAD:0:8}" + CHANGED=$(git diff --name-only "${BASE}" "${HEAD}" 2>/dev/null || git diff --name-only HEAD~1 HEAD) + echo "Changed files:" + echo "${CHANGED}" | sed 's/^/ /' + + # Classify + FE=false; BE=false; INFRA=false; DOCS=false; OTHER=false + while IFS= read -r file; do + [[ -z "$file" ]] && continue + case "$file" in + src/js/*|src/css/*|src/editor/*|src/index.html|src/sw.js|src/manifest.json|src/vendor/*) + FE=true ;; + server/*|scripts/db-*) + BE=true ;; + .gitea/*|k8s/*|Dockerfile*|docker-compose*|docker-entrypoint*|nginx.conf) + INFRA=true ;; + docs/*|*.md|CHANGELOG.md|LICENSE) + DOCS=true ;; + VERSION|scripts/*) + FE=true; BE=true ;; + *) + OTHER=true ;; + esac + done <<< "${CHANGED}" + + # Docs-only: only docs changed, nothing else + if [[ "$DOCS" == "true" && "$FE" == "false" && "$BE" == "false" && "$INFRA" == "false" && "$OTHER" == "false" ]]; then + DOCS_ONLY=true + else + DOCS_ONLY=false + fi + + echo "frontend=${FE}" >> "$GITHUB_OUTPUT" + echo "backend=${BE}" >> "$GITHUB_OUTPUT" + echo "infra=${INFRA}" >> "$GITHUB_OUTPUT" + echo "docs_only=${DOCS_ONLY}" >> "$GITHUB_OUTPUT" + + echo "" + echo "━━━ Change Detection ━━━" + echo " frontend: ${FE}" + echo " backend: ${BE}" + echo " infra: ${INFRA}" + echo " docs_only: ${DOCS_ONLY}" + # ── Stage 1a: Frontend Tests ───────────────── # API contract tests, model processing, policy wiring audits. # Uses Node.js built-in test runner (node --test), zero npm deps. + # + # Runs when: frontend files changed, infra changed, or not docs-only. + # Skipped when: only backend or docs changed. test-frontend: runs-on: ubuntu-latest + needs: [detect-changes] + if: needs.detect-changes.outputs.frontend == 'true' || needs.detect-changes.outputs.infra == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -106,8 +203,13 @@ jobs: run: node --test src/js/__tests__/*.test.js # ── Stage 1b: Go Build & Test (Postgres) ───── + # + # Runs when: backend files changed or infra changed. + # Skipped when: only frontend or docs changed. test: runs-on: ubuntu-latest + needs: [detect-changes] + if: needs.detect-changes.outputs.backend == 'true' || needs.detect-changes.outputs.infra == 'true' env: GOPRIVATE: git.gobha.me/* GONOSUMCHECK: git.gobha.me/* @@ -190,8 +292,13 @@ jobs: # Verifies SQLite backend compiles, stores work, and handler # integration tests pass against an in-memory SQLite database. # No external database or provider keys required. + # + # Runs when: backend files changed or infra changed. + # Skipped when: only frontend or docs changed. test-sqlite: runs-on: ubuntu-latest + needs: [detect-changes] + if: needs.detect-changes.outputs.backend == 'true' || needs.detect-changes.outputs.infra == 'true' env: GOPRIVATE: git.gobha.me/* GONOSUMCHECK: git.gobha.me/* @@ -244,9 +351,19 @@ jobs: echo "✓ SQLite integration tests complete" # ── Stage 2: Build, Database, Deploy ───────── + # + # Depends on all test jobs. Skipped jobs (due to path gating) + # are treated as successful — no blocking. + # + # Skipped entirely for docs-only changes (nothing to build). build-and-deploy: runs-on: ubuntu-latest - needs: [test, test-frontend, test-sqlite] + needs: [detect-changes, test, test-frontend, test-sqlite] + # Run unless: a needed job failed, the workflow was cancelled, or it's docs-only. + # Skipped test jobs (path-gated) are fine — they don't block. + if: | + !cancelled() && !failure() && + needs.detect-changes.outputs.docs_only != 'true' steps: - name: Checkout uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index be2c6d9..a7d7e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,71 @@ All notable changes to Chat Switchboard. +## [0.17.2] — 2026-02-28 + +### Added +- **CodeMirror 6 integration.** Rich editor infrastructure compiled at Docker + build time via esbuild (IIFE bundle, ~295KB min / ~90KB gzip). Two factory + functions exposed on `window.CM`: + - `CM.chatInput()` — Markdown-mode editor for the chat input with + auto-growing height, Enter=send / Shift+Enter=newline, spell check, and + WYSIWYG fenced code block decorations (visual container with monospace + font and accent border, matching claude.ai UX). + - `CM.codeEditor()` — Full-featured code editor for admin extension panel + with line numbers, bracket matching, search/replace, fold gutter, and 10 + bundled language modes (Markdown, JavaScript, JSON, SQL, HTML, CSS, YAML, + Go, Python, Rust). +- **Graceful degradation.** All CM6 integration points check `window.CM` + availability. If the bundle fails to load, the app falls back to native + ` +``` +To: +```html +
+``` + +With graceful fallback: if `window.CM?.noteEditor` is undefined, create a +` +
+ +
` : ''; + // Use container divs instead of textareas when CM6 is available + const useCM6 = !!window.CM?.codeEditor; + const manifestEditor = useCM6 + ? `
` + : ``; + const scriptEditor = useCM6 + ? `
` + : ``; + const formHTML = `
${systemWarning} @@ -729,14 +769,14 @@ function editAdminExtension(id) {
- + ${manifestEditor}
- + ${scriptEditor}
- +
@@ -748,42 +788,80 @@ function editAdminExtension(id) { if (row.querySelector(`[onclick*="editAdminExtension('${id}')"]`)) { row.insertAdjacentHTML('afterend', formHTML); - // Tab key inserts tab in script textarea - const scriptEl = document.getElementById(`extEdit-script-${id}`); - if (scriptEl) { - scriptEl.addEventListener('keydown', (e) => { - if (e.key === 'Tab') { - e.preventDefault(); - const start = scriptEl.selectionStart; - const end = scriptEl.selectionEnd; - scriptEl.value = scriptEl.value.substring(0, start) + ' ' + scriptEl.value.substring(end); - scriptEl.selectionStart = scriptEl.selectionEnd = start + 4; - } - }); + if (useCM6) { + // Get user keybinding preference + const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}'); + const keymapMode = prefs.editorKeymap || 'standard'; + + // Initialize CM6 editors + _extEditors[id] = { + manifest: CM.codeEditor(document.getElementById(`extEdit-manifest-${id}`), { + language: 'json', + value: manifestJSON, + lineNumbers: true, + keymap: keymapMode, + }), + script: CM.codeEditor(document.getElementById(`extEdit-script-${id}`), { + language: 'javascript', + value: script, + lineNumbers: true, + keymap: keymapMode, + }), + }; + } else { + // Fallback: Tab key inserts tab in script textarea + const scriptEl = document.getElementById(`extEdit-script-${id}`); + if (scriptEl) { + scriptEl.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + e.preventDefault(); + const start = scriptEl.selectionStart; + const end = scriptEl.selectionEnd; + scriptEl.value = scriptEl.value.substring(0, start) + ' ' + scriptEl.value.substring(end); + scriptEl.selectionStart = scriptEl.selectionEnd = start + 4; + } + }); + } } break; } } } +function _closeExtEditForm(id) { + if (_extEditors[id]) { + _extEditors[id].manifest?.destroy(); + _extEditors[id].script?.destroy(); + delete _extEditors[id]; + } + document.querySelector(`.ext-edit-form[data-ext-id="${id}"]`)?.remove(); +} + async function saveAdminExtension(id) { const nameEl = document.getElementById(`extEdit-name-${id}`); const descEl = document.getElementById(`extEdit-desc-${id}`); - const manifestEl = document.getElementById(`extEdit-manifest-${id}`); - const scriptEl = document.getElementById(`extEdit-script-${id}`); - if (!nameEl || !manifestEl || !scriptEl) return; + + // Read from CM6 if available, otherwise from textarea + const editors = _extEditors[id]; + const manifestText = editors?.manifest + ? editors.manifest.getValue() + : document.getElementById(`extEdit-manifest-${id}`)?.value; + const scriptText = editors?.script + ? editors.script.getValue() + : document.getElementById(`extEdit-script-${id}`)?.value; + + if (!nameEl || manifestText == null || scriptText == null) return; // Parse manifest and merge _script back in let manifest; try { - manifest = JSON.parse(manifestEl.value.trim() || '{}'); + manifest = JSON.parse((manifestText || '').trim() || '{}'); } catch (e) { return UI.toast('Invalid manifest JSON: ' + e.message, 'error'); } - const script = scriptEl.value; - if (script.trim()) { - manifest._script = script; + if (scriptText.trim()) { + manifest._script = scriptText; } try { @@ -793,6 +871,7 @@ async function saveAdminExtension(id) { manifest: manifest, }); UI.toast('Extension updated — reload page to apply changes'); + _closeExtEditForm(id); await UI.loadAdminExtensions(); } catch (e) { UI.toast(e.message, 'error'); diff --git a/src/js/attachments.js b/src/js/attachments.js index f23097c..39cb774 100644 --- a/src/js/attachments.js +++ b/src/js/attachments.js @@ -721,7 +721,6 @@ function initAttachments() { function _initAttachmentListeners() { const attachBtn = document.getElementById('attachBtn'); const fileInput = document.getElementById('fileInput'); - const messageInput = document.getElementById('messageInput'); const chatArea = document.getElementById('chatMessages'); const lightbox = document.getElementById('lightbox'); @@ -741,8 +740,12 @@ function _initAttachmentListeners() { } // ── Smart Paste ───────────────────────── - if (messageInput) { - messageInput.addEventListener('paste', (e) => { + // Attach to the active input element (CM6 contentDOM or textarea) + const pasteTarget = (typeof ChatInput !== 'undefined' && ChatInput.getDom()) + ? ChatInput.getDom() + : document.getElementById('messageInput'); + if (pasteTarget) { + pasteTarget.addEventListener('paste', (e) => { if (!App.storageConfigured) return; // fall through to normal paste const items = [...(e.clipboardData?.items || [])]; diff --git a/src/js/chat.js b/src/js/chat.js index 1dacb7c..fdb88d9 100644 --- a/src/js/chat.js +++ b/src/js/chat.js @@ -4,6 +4,75 @@ // Chat management, send, stream, regenerate, edit, branch, // summarize, per-chat model persistence. +// ── Chat Input Abstraction ────────────────── +// Wraps CM6 editor (when available) or fallback textarea. +// All code that reads/writes the message input goes through this. + +const ChatInput = { + _editor: null, // CM6 instance, set during init + _textarea: null, // fallback textarea element + + getValue() { + if (this._editor) return this._editor.getValue(); + return this._textarea?.value || ''; + }, + + setValue(text) { + if (this._editor) { + this._editor.setValue(text || ''); + } else if (this._textarea) { + this._textarea.value = text || ''; + this._textarea.style.height = 'auto'; + } + }, + + focus() { + if (this._editor) this._editor.focus(); + else this._textarea?.focus(); + }, + + /** Get the DOM element (for paste listeners, etc.) */ + getDom() { + if (this._editor) return this._editor.getView().contentDOM; + return this._textarea; + }, + + /** Get the wrapper DOM element (for resize, etc.) */ + getWrapDom() { + if (this._editor) return this._editor.getView().dom; + return this._textarea; + }, + + /** Initialize — try CM6, fall back to textarea */ + init() { + this._textarea = document.getElementById('messageInput'); + const wrap = document.getElementById('messageInputWrap'); + + if (window.CM?.chatInput && wrap) { + // Hide the textarea, create CM6 editor in its place + this._textarea.style.display = 'none'; + this._editor = CM.chatInput(wrap, { + placeholder: 'Send a message...', + onSubmit: () => sendMessage(), + onChange: () => updateInputTokens(), + maxHeight: 200, + }); + DebugLog?.push?.('chat', '[CM6] Chat input initialized'); + } else { + // Fallback: textarea with manual event handling + DebugLog?.push?.('chat', '[CM6] Not available — using textarea fallback'); + this._textarea.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } + }); + this._textarea.addEventListener('input', function() { + this.style.height = 'auto'; + this.style.height = Math.min(this.scrollHeight, 200) + 'px'; + updateInputTokens(); + }); + } + }, +}; + // ── Summarize & Continue ────────────────── async function summarizeAndContinue() { @@ -266,7 +335,7 @@ async function newChat() { Tokens._warningDismissed = false; updateContextWarning(); updateChatTokenCount(); updateInputTokens(); - document.getElementById('messageInput').focus(); + ChatInput.focus(); if (window.innerWidth <= 768) { document.getElementById('sidebar').classList.add('collapsed'); const ov = document.getElementById('sidebarOverlay'); @@ -377,15 +446,13 @@ function startRenameChat(chatId) { // ── Send Message ───────────────────────────── async function sendMessage() { - const input = document.getElementById('messageInput'); - const text = input.value.trim(); + const text = ChatInput.getValue().trim(); const hasAttachments = hasStagedAttachments(); // Need text or attachments, not generating, not blocked by uploads if ((!text && !hasAttachments) || App.isGenerating || isSendBlocked()) return; - input.value = ''; - input.style.height = 'auto'; + ChatInput.setValue(''); // Snapshot attachment IDs before clearing staged state const attachmentIds = getStagedAttachmentIds(); @@ -749,16 +816,8 @@ function _initChatListeners() { UI.toast(`Loaded ${visible} model${visible !== 1 ? 's' : ''}`, 'success'); }); - // Input - const input = document.getElementById('messageInput'); - input.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } - }); - input.addEventListener('input', function() { - this.style.height = 'auto'; - this.style.height = Math.min(this.scrollHeight, 200) + 'px'; - updateInputTokens(); - }); + // Input — CM6 or textarea fallback + ChatInput.init(); // Close modals on overlay click document.querySelectorAll('.modal-overlay').forEach(overlay => { diff --git a/src/js/debug.js b/src/js/debug.js index f341f2c..245419b 100644 --- a/src/js/debug.js +++ b/src/js/debug.js @@ -272,6 +272,11 @@ const DebugLog = { snap.extensions = Extensions.debug(); } + // CM6 editor state + snap.cm6 = window.CM + ? { version: window.CM.version, languages: Object.keys(window.CM.languages || {}) } + : { available: false }; + return snap; }, diff --git a/src/js/tokens.js b/src/js/tokens.js index a33f3b4..8cdc1a1 100644 --- a/src/js/tokens.js +++ b/src/js/tokens.js @@ -69,8 +69,7 @@ function updateInputTokens() { const el = document.getElementById('inputTokenCount'); if (!el) return; - const input = document.getElementById('messageInput'); - const inputText = input?.value || ''; + const inputText = (typeof ChatInput !== 'undefined') ? ChatInput.getValue() : ''; const inputTokens = Tokens.estimate(inputText); if (!inputText.trim()) { diff --git a/src/js/ui-settings.js b/src/js/ui-settings.js index b4e3b76..351a501 100644 --- a/src/js/ui-settings.js +++ b/src/js/ui-settings.js @@ -11,17 +11,30 @@ Object.assign(UI, { const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}'); const scale = prefs.scale || 100; const msgFont = prefs.msgFont || 14; + const theme = prefs.theme || 'dark'; + const keymap = prefs.editorKeymap || 'standard'; const scaleEl = document.getElementById('settingsScale'); const msgFontEl = document.getElementById('settingsMsgFont'); if (scaleEl) { scaleEl.value = scale; document.getElementById('scaleValue').textContent = scale + '%'; } if (msgFontEl) { msgFontEl.value = msgFont; document.getElementById('msgFontValue').textContent = msgFont + 'px'; } + + // Highlight active theme button + document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.theme === theme); + }); + + // Highlight active keymap button + document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.keymap === keymap); + }); }, initAppearance() { // Load saved prefs on startup const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}'); UI.applyAppearance(prefs.scale || 100, prefs.msgFont || 14); + UI.applyTheme(prefs.theme || 'dark'); // Live preview on slider change const scaleEl = document.getElementById('settingsScale'); @@ -36,6 +49,70 @@ Object.assign(UI, { document.getElementById('msgFontValue').textContent = v + 'px'; UI.applyAppearance(parseInt(scaleEl?.value || 100), v); }); + + // Theme toggle buttons + document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => { + btn.addEventListener('click', () => { + const theme = btn.dataset.theme; + UI.applyTheme(theme); + document.querySelectorAll('#themeToggle .theme-btn').forEach(b => + b.classList.toggle('active', b === btn) + ); + const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}'); + p.theme = theme; + localStorage.setItem('cs-appearance', JSON.stringify(p)); + }); + }); + + // Editor keymap toggle buttons + document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => { + btn.addEventListener('click', () => { + const mode = btn.dataset.keymap; + document.querySelectorAll('#keymapToggle .theme-btn').forEach(b => + b.classList.toggle('active', b === btn) + ); + // Persist + const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}'); + p.editorKeymap = mode; + localStorage.setItem('cs-appearance', JSON.stringify(p)); + // Notify open editors + if (typeof Events !== 'undefined' && Events.emit) { + Events.emit('keymap.changed', { mode }); + } + }); + }); + + // System theme change listener (for "system" mode) + UI._systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)'); + UI._systemThemeQuery.addEventListener('change', () => { + const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}'); + if (p.theme === 'system') UI.applyTheme('system'); + }); + }, + + /** + * Apply theme to the document. + * @param {'light'|'dark'|'system'} mode + */ + applyTheme(mode) { + let resolved = mode; + if (mode === 'system') { + resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + if (resolved === 'light') { + document.documentElement.setAttribute('data-theme', 'light'); + } else { + document.documentElement.removeAttribute('data-theme'); + } + // Publish event for CM6 editors and extensions + if (typeof Events !== 'undefined' && Events.emit) { + Events.emit('theme.changed', { mode, resolved }); + } + }, + + /** Get the currently resolved theme ('dark' or 'light') */ + getResolvedTheme() { + return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark'; }, applyAppearance(scale, msgFont) { diff --git a/src/sw.js b/src/sw.js index c8b239e..17ab62e 100644 --- a/src/sw.js +++ b/src/sw.js @@ -68,11 +68,12 @@ self.addEventListener('activate', (event) => { self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); - // Never cache API calls, WebSocket upgrades, branding, or extension assets + // Never cache API calls, WebSocket upgrades, branding, extensions, or CM6 bundle if (url.pathname.includes('/api/') || url.pathname.includes('/ws') || url.pathname.includes('/branding/') || url.pathname.includes('/extensions/') || + url.pathname.includes('/vendor/codemirror/') || event.request.method !== 'GET') { return; }