diff --git a/static/app.js b/static/app.js
index e14121adb..8454c3c65 100644
--- a/static/app.js
+++ b/static/app.js
@@ -12,7 +12,7 @@ import presetsModule from './js/presets.js?v=20260908personaname1';
import searchModule from './js/search.js';
import chatModule from './js/chat.js?v=20260917toolttft1';
import compareModule from './js/compare/index.js?v=20260909mobilepaneaddscroll1';
-import documentModule from './js/document.js?v=20260916docctx2';
+import documentModule from './js/document.js?v=20261028emailpickerfiles1';
import searchChatModule from './js/search-chat.js';
import { makeWindowDraggable } from './js/windowDrag.js';
import {
diff --git a/static/index.html b/static/index.html
index 901b42510..a8a5d1f03 100644
--- a/static/index.html
+++ b/static/index.html
@@ -2682,7 +2682,7 @@
-
+
diff --git a/static/js/chat.js b/static/js/chat.js
index b02eb743c..91ce0d886 100644
--- a/static/js/chat.js
+++ b/static/js/chat.js
@@ -16,7 +16,7 @@ import spinnerModule from './spinner.js';
import presetsModule from './presets.js?v=20260908personaname1';
import fileHandlerModule from './fileHandler.js?v=20260909mobileattachmentedit1';
import searchModule from './search.js';
-import documentModule from './document.js?v=20260916docctx2';
+import documentModule from './document.js?v=20261028emailpickerfiles1';
import * as emailInbox from './emailInbox.js?v=20260914aireply4';
import codeRunnerModule from './codeRunner.js?v=20260831richtexttools91';
import slashCommands, { initSlashCommands, isCommand, handleSlashCommand, handleSetupInput, handleSetupWizard, typewriterInto } from './slashCommands.js?v=20260921chatgptusage1';
diff --git a/static/js/chatRenderer.js b/static/js/chatRenderer.js
index dceec9b24..cb2f266b6 100644
--- a/static/js/chatRenderer.js
+++ b/static/js/chatRenderer.js
@@ -1889,7 +1889,7 @@ function _activateEntityAnchor(e, forcedAnchor = null) {
} catch {}
});
} else if (kind === 'document') {
- import('./document.js?v=20260916docctx2').then(mod => {
+ import('./document.js?v=20261028emailpickerfiles1').then(mod => {
const open = mod.loadDocument
|| mod.openDocument
|| (mod.default && (mod.default.loadDocument || mod.default.openDocument));
@@ -3656,7 +3656,7 @@ export function addMessage(role, content, modelName, metadata) {
b.querySelectorAll('[data-doc-edit-ref]').forEach(button => {
button.addEventListener('click', () => {
- import('./document.js?v=20260916docctx2').then(mod => {
+ import('./document.js?v=20261028emailpickerfiles1').then(mod => {
const restore = mod.restoreSelectionReference
|| mod.default?.restoreSelectionReference;
return restore?.(button.dataset.docEditRef || '', {
diff --git a/static/js/chatStream.js b/static/js/chatStream.js
index 4981ac34d..c048905d7 100644
--- a/static/js/chatStream.js
+++ b/static/js/chatStream.js
@@ -7,7 +7,7 @@ import Storage from './storage.js';
import themeModule from './theme.js?v=20260911organsrain1';
import markdownModule from './markdown.js';
import sessionModule from './sessions.js';
-import documentModule from './document.js?v=20260916docctx2';
+import documentModule from './document.js?v=20261028emailpickerfiles1';
// Tool approvals are control-plane submits for the current chat. chat.js
// deliberately leaves the composer untouched, then programmatically clicks the
diff --git a/static/js/document.js b/static/js/document.js
index a3e01dc45..0aa6bad98 100644
--- a/static/js/document.js
+++ b/static/js/document.js
@@ -5197,7 +5197,7 @@ import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1';
}
async function _handleAttachUpload(e) {
- const files = e.target.files;
+ const files = Array.from(e.target.files || []);
e.target.value = ''; // reset for next upload
await _uploadComposeFiles(files);
}
diff --git a/static/js/emailLibrary/attachments.js b/static/js/emailLibrary/attachments.js
index 1af364e15..792a881c1 100644
--- a/static/js/emailLibrary/attachments.js
+++ b/static/js/emailLibrary/attachments.js
@@ -187,7 +187,7 @@ function _wireAttachmentHandlers(reader, folder) {
ownerModal.classList.add('hidden');
}
}
- const docMod = await import('../document.js?v=20260916docctx2');
+ const docMod = await import('../document.js?v=20261028emailpickerfiles1');
const load = (docMod && docMod.loadDocument) || (docMod && docMod.default && docMod.default.loadDocument);
if (typeof load === 'function') {
await load(json.doc_id);
diff --git a/static/js/slashCommands.js b/static/js/slashCommands.js
index d288cb3a1..a3c793487 100644
--- a/static/js/slashCommands.js
+++ b/static/js/slashCommands.js
@@ -16,7 +16,7 @@ import modelsModule from './models.js';
import chatRenderer from './chatRenderer.js?v=20260914metricssummary1';
import spinnerModule from './spinner.js';
import themeModule from './theme.js?v=20260911organsrain1';
-import documentModule from './document.js?v=20260916docctx2';
+import documentModule from './document.js?v=20261028emailpickerfiles1';
import workspaceModule from './workspace.js';
import settingsModule from './settings.js?v=20260912writingstyle3';
import cookbookModule from './cookbook.js';
diff --git a/static/sw.js b/static/sw.js
index 44cf2e2a1..51f98564e 100644
--- a/static/sw.js
+++ b/static/sw.js
@@ -7,7 +7,7 @@
// - Other static assets (images/fonts/libs): cache-first with bg refresh.
// - API / non-GET: never cached.
// Bump CACHE_NAME whenever the precache list or SW logic changes.
-const CACHE_NAME = 'odysseus-v659-notes-reference';
+const CACHE_NAME = 'odysseus-v660-email-picker-files';
// KaTeX resolves these from its own stylesheet, so caching the CSS without them
// gives offline math fallback glyphs instead of proper typesetting.
@@ -80,7 +80,7 @@ const PRECACHE = [
'/static/js/search.js',
'/static/js/spinner.js',
'/static/js/tts-ai.js',
- '/static/js/document.js?v=20260916docctx2',
+ '/static/js/document.js?v=20261028emailpickerfiles1',
'/static/js/gallery.js?v=20260910promptcopy1',
'/static/js/chatRenderer.js?v=20260914metricssummary1',
'/static/js/codeRunner.js?v=20260831richtexttools91',
diff --git a/tests/test_email_paperclip_files_browser.py b/tests/test_email_paperclip_files_browser.py
new file mode 100644
index 000000000..c0f753e3e
--- /dev/null
+++ b/tests/test_email_paperclip_files_browser.py
@@ -0,0 +1,55 @@
+"""The native FileList must survive resetting the compose file input."""
+import json
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+from tests.helpers.document_source import function_body
+
+
+@pytest.mark.parametrize("selections", [[["one.txt"]], [["one.txt", "two.csv"]], [["one.txt"], ["one.txt"]], [[]]])
+def test_compose_file_selection_survives_reset(tmp_path, selections):
+ if not shutil.which("node"):
+ pytest.skip("node not installed")
+ for name, content in [("one.txt", "Synthetic attachment one\n"), ("two.csv", 'name,value\n"fixture",2\n')]:
+ (tmp_path / name).write_text(content, encoding="utf-8")
+ script = r"""
+const fs=require('node:fs'),assert=require('node:assert/strict');
+let chromium;try{({chromium}=require('playwright'));}catch(e){if(e.code==='MODULE_NOT_FOUND')process.exit(77);throw e;}
+(async()=>{
+ const browser=await chromium.launch({headless:true});
+ try{
+ const page=await browser.newPage();const errors=[];page.on('pageerror',e=>errors.push(e.message));
+ await page.setContent('');
+ await page.addScriptTag({content:`
+ window.uploads=[];window.finished=0;
+ async function _uploadComposeFiles(files){
+ for(const file of files)window.uploads.push({name:file.name,text:await file.text()});
+ }
+ __HANDLER__
+ document.querySelector('input').addEventListener('change',async event=>{
+ await _handleAttachUpload(event);window.finished++;
+ });
+ `});
+ const selections=JSON.parse(process.argv[1]),root=process.argv[2],expected=[];
+ for(const [index,names] of selections.entries()){
+ await page.getByLabel('Compose attachments').setInputFiles(names.map(name=>root+'/'+name));
+ await page.waitForFunction(n=>window.finished===n,index+1);
+ expected.push(...names.map(name=>({name,text:fs.readFileSync(root+'/'+name,'utf8')})));
+ assert.deepEqual(await page.evaluate(()=>window.uploads),expected);
+ assert.equal(await page.getByLabel('Compose attachments').inputValue(),'');
+ assert.equal(await page.getByLabel('Compose attachments').evaluate(el=>el.files.length),0);
+ }
+ assert.deepEqual(errors,[]);console.log(JSON.stringify({files:expected.length}));
+ }finally{await browser.close();}
+})().catch(e=>{console.error(e);process.exitCode=1;});
+""".replace("__HANDLER__", function_body("_handleAttachUpload"))
+ result = subprocess.run(["node", "-e", script, json.dumps(selections), str(tmp_path)],
+ cwd=Path(__file__).resolve().parents[1], capture_output=True,
+ text=True, timeout=30)
+ if result.returncode == 77:
+ pytest.skip("playwright not installed")
+ assert result.returncode == 0, result.stdout + result.stderr
+ assert json.loads(result.stdout)["files"] == sum(map(len, selections))