From 2a16ee3fdce7868b899e5cd87f7f9f5b94a447eb Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 18:53:40 +0800 Subject: [PATCH 01/10] perf(editor): register ranui select when the assistant loads Only the assistant renders r-select, and its modules import the element themselves, so the editor no longer loads it at startup. Co-Authored-By: Claude Opus 5.5 --- index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/index.ts b/index.ts index 68ff6862..135c32ff 100644 --- a/index.ts +++ b/index.ts @@ -20,7 +20,6 @@ import { initAnalytics } from './lib/analytics'; import { createControlPanel, hideControlPanel, hideLanding, showControlPanel } from './lib/ui'; import 'ranui/button'; import 'ranui/card'; -import 'ranui/select'; import { initWebMcp } from './lib/web-mcp'; import { createAgentEntry } from './lib/agent-entry'; import { hasUnsavedChanges, installUnsavedChangesGuard } from './lib/unsaved-guard'; From 7130049cda9b2c7f2354620123f74a123928ca2b Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 18:53:40 +0800 Subject: [PATCH 02/10] refactor(shared): remove the unused getMimeTypeFromExtension Co-Authored-By: Claude Opus 5.5 --- packages/shared/CLAUDE.md | 8 +------- packages/shared/README.md | 4 ++-- packages/shared/src/document-utils.ts | 12 ------------ test/unit/document-utils.test.ts | 17 +---------------- test/unit/onlyoffice-editor.test.ts | 4 ---- 5 files changed, 4 insertions(+), 41 deletions(-) diff --git a/packages/shared/CLAUDE.md b/packages/shared/CLAUDE.md index 02dc32d9..76eaa541 100644 --- a/packages/shared/CLAUDE.md +++ b/packages/shared/CLAUDE.md @@ -7,13 +7,7 @@ not generic. Editor / converter / agent layers all import from here. ## Import (prefer subpaths) ```ts -import { - getDocumentType, - getMimeTypeFromExtension, - parseReadonly, - BASE_PATH, - DOCUMENT_TYPE_MAP, -} from '@ranuts/shared/document-utils'; +import { getDocumentType, parseReadonly, BASE_PATH, DOCUMENT_TYPE_MAP } from '@ranuts/shared/document-utils'; import type { DocumentType, ConversionResult, diff --git a/packages/shared/README.md b/packages/shared/README.md index fcac3e46..a8457cd2 100644 --- a/packages/shared/README.md +++ b/packages/shared/README.md @@ -16,7 +16,7 @@ pnpm add @ranuts/shared Prefer subpaths; `.` is a barrel that re-exports all of them. ```ts -import { getDocumentType, getMimeTypeFromExtension, parseReadonly, BASE_PATH } from '@ranuts/shared/document-utils'; +import { getDocumentType, parseReadonly, BASE_PATH } from '@ranuts/shared/document-utils'; import type { DocumentType, ConversionResult } from '@ranuts/shared/document-types'; import { t, getLanguage, setLanguage, getOnlyOfficeLang } from '@ranuts/shared/i18n'; import { getDocmentObj, setDocmentObj } from '@ranuts/shared/store'; @@ -26,7 +26,7 @@ import { getDocmentObj, setDocmentObj } from '@ranuts/shared/store'; | Subpath | Exports | | ------------------ | ------------------------------------------------------------------------------------------------------------------ | -| `./document-utils` | `getDocumentType`, `getMimeTypeFromExtension`, `getBasePath`, `BASE_PATH`, `DOCUMENT_TYPE_MAP`, `parseReadonly` | +| `./document-utils` | `getDocumentType`, `getDocumentMimeType`, `getBasePath`, `BASE_PATH`, `DOCUMENT_TYPE_MAP`, `parseReadonly` | | `./document-types` | `DocumentType`, `ConversionResult`, `BinConversionResult`, `SaveEvent`, `EmscriptenModule`, `EmscriptenFileSystem` | | `./i18n` | `t`, `getLanguage`, `setLanguage`, `getOnlyOfficeLang`, `Language`, `I18nMessages` (zh/en/ja/ko/de/fr/es/pt/ru) | | `./store` | `getDocmentObj`, `setDocmentObj` (ranuts signal over `{ fileName, file?, url? }`) | diff --git a/packages/shared/src/document-utils.ts b/packages/shared/src/document-utils.ts index 2721a0ad..554b58c0 100644 --- a/packages/shared/src/document-utils.ts +++ b/packages/shared/src/document-utils.ts @@ -1,4 +1,3 @@ -import { getMime } from 'ranuts/utils'; import type { DocumentType } from './document-types'; /** @@ -39,17 +38,6 @@ export function getDocumentType(fileType: string): string | null { return Object.hasOwn(DOCUMENT_TYPE_MAP, type) ? DOCUMENT_TYPE_MAP[type] : null; } -/** - * Get MIME type from file extension (using ranuts getMime utility) - * @param extension - File extension - * @returns string - MIME type - */ -export function getMimeTypeFromExtension(extension: string): string { - // Use ranuts getMime for common image types, fallback to image/png - const mime = getMime(extension?.toLowerCase() || ''); - return mime || 'image/png'; -} - // Canonical MIME map for the document formats this project saves/exports. // Single source of truth -- lib/onlyoffice-editor.ts and @ranuts/converter // both delegate here instead of keeping their own copies. diff --git a/test/unit/document-utils.test.ts b/test/unit/document-utils.test.ts index e266cf31..a8c37269 100644 --- a/test/unit/document-utils.test.ts +++ b/test/unit/document-utils.test.ts @@ -1,11 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { - DOCUMENT_TYPE_MAP, - getBasePath, - getDocumentType, - getMimeTypeFromExtension, - parseReadonly, -} from '@ranuts/shared/document-utils'; +import { DOCUMENT_TYPE_MAP, getBasePath, getDocumentType, parseReadonly } from '@ranuts/shared/document-utils'; describe('document utils', () => { it('classifies common document extensions', () => { @@ -27,15 +21,6 @@ describe('document utils', () => { expect(getDocumentType(extension)).toBe(kind); }); - it('normalizes extension casing for MIME lookup', () => { - expect(getMimeTypeFromExtension('XLSX')).toBe('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - }); - - it('uses image/png fallback for unknown MIME extensions', () => { - expect(getMimeTypeFromExtension('unknown')).toBe('image/png'); - expect(getMimeTypeFromExtension('')).toBe('image/png'); - }); - it('parses the readonly query value for preview mode', () => { // Truthy forms expect(parseReadonly('true')).toBe(true); diff --git a/test/unit/onlyoffice-editor.test.ts b/test/unit/onlyoffice-editor.test.ts index 16eeb736..2dcdc7fe 100644 --- a/test/unit/onlyoffice-editor.test.ts +++ b/test/unit/onlyoffice-editor.test.ts @@ -13,10 +13,6 @@ vi.mock('@ranuts/shared/i18n', () => ({ // caller passed; that the real locale strings fill them is i18n.test.ts. t: vi.fn((key: string, vars?: Record) => (vars ? `${key}${JSON.stringify(vars)}` : key)), })); -vi.mock(import('@ranuts/shared/document-utils'), async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, getMimeTypeFromExtension: vi.fn().mockReturnValue('image/png') }; -}); import { awaitFontSystem, From c899dd5a90dae7663727460ce98f34c23ead658d Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 18:53:40 +0800 Subject: [PATCH 03/10] test(e2e): keep ranui select out of the editor startup scripts Co-Authored-By: Claude Opus 5.5 --- test/e2e/editor-startup-payload.spec.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/e2e/editor-startup-payload.spec.ts b/test/e2e/editor-startup-payload.spec.ts index 78cbf47c..d4c400f0 100644 --- a/test/e2e/editor-startup-payload.spec.ts +++ b/test/e2e/editor-startup-payload.spec.ts @@ -15,6 +15,8 @@ const JSCHARDET = { owner: 'jschardet', probe: 'windows-1255' }; // An error the save pipeline (lib/onlyoffice/save-stream.ts) raises: the // history page lists and reopens records, it never saves through the editor. const EDITOR_STACK = { owner: 'editor save pipeline', probe: 'Save conversion failed' }; +// ranui's select; only the assistant renders one, and it imports the element itself. +const RANUI_SELECT = { owner: 'ranui select', probe: 'ran-select-dropdown-option' }; // Two message tables the page does not speak: only English and the active // locale belong at startup (packages/shared/src/i18n.ts awaits the latter). const OTHER_LOCALES = [ @@ -43,11 +45,9 @@ async function expectAbsent(page: Page, deferred: Array<{ owner: string; probe: } } -test('the editor startup scripts leave the agent tools, the PWA prompt, jschardet and other locales out', async ({ - page, -}) => { +test('the editor startup scripts leave optional modules and other locales out', async ({ page }) => { await page.goto('/editor?new=docx'); - await expectAbsent(page, [AGENT_TOOLS, PWA_INSTALL, JSCHARDET, ...OTHER_LOCALES]); + await expectAbsent(page, [AGENT_TOOLS, PWA_INSTALL, JSCHARDET, RANUI_SELECT, ...OTHER_LOCALES]); }); test('the history page startup scripts leave the editor stack, jschardet and other locales out', async ({ page }) => { @@ -67,3 +67,15 @@ test('the PWA install element is still defined and mounted after startup', async await expect(page.locator('pwa-install#pwa-install')).toBeAttached({ timeout: 15_000 }); await expect.poll(() => page.evaluate(() => Boolean(customElements.get('pwa-install')))).toBe(true); }); + +test('the select element is defined once the assistant opens, without being in startup', async ({ page }) => { + test.setTimeout(90_000); + await page.addInitScript(() => localStorage.setItem('document-assistant-enabled', 'true')); + await page.goto('/editor?new=docx&locale=en'); + await page.waitForFunction(() => Boolean(window.__ooFrames.readyEditor())); + expect(await page.evaluate(() => Boolean(customElements.get('r-select')))).toBe(false); + await page.frameLocator('iframe[name="frameEditor"]').locator('.agent-sidebar-entry').click(); + await page.locator('.agent-panel-settings-toggle').click(); + await expect.poll(() => page.evaluate(() => Boolean(customElements.get('r-select')))).toBe(true); + await expect(page.locator('.agent-panel-settings r-select:visible').first()).toBeVisible(); +}); From 32452fd39f3f1dc2b69b90b5ecff8b420e140c60 Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 18:53:40 +0800 Subject: [PATCH 04/10] docs(perf): record the select round and the ranui MIME table holder Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 + .../2026-10-24-select-and-mime-table.md | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 docs/explorations/2026-10-24-select-and-mime-table.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d08b067..69d7e48d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,6 +199,8 @@ notes. Entries describe what users experience, not internal refactors. - The editor and history pages load only the interface language you use rather than all seven, so the editor starts on 105 KB instead of 162 KB compressed, and the history page on 61 KB instead of 121 KB. +- The editor no longer loads the assistant's dropdown control before the + assistant opens (90 KB instead of 105 KB compressed at startup). - Voice controls stay in place while recording or finishing. Explanations appear without moving the draft, and cancelling preserves the text typed before dictation. - **Configured assistants reopen in chat.** Enable a downloaded model or reconnect diff --git a/docs/explorations/2026-10-24-select-and-mime-table.md b/docs/explorations/2026-10-24-select-and-mime-table.md new file mode 100644 index 00000000..9e4ab686 --- /dev/null +++ b/docs/explorations/2026-10-24-select-and-mime-table.md @@ -0,0 +1,75 @@ +# Startup payload, round four: ranui select, and why the MIME table stays + +Follow-up to `2026-10-24-active-locale-only.md`. After the locale split, two +more startup items were found by attributing startup bytes to their source +packages through the sourcemaps. ranui's `select` is about 7.6 KB gzip on its +own and is on the editor page. ranuts' full MIME table +(`ranuts/dist/src/utils/mimeType.js`, 30 KB raw / 6.8 KB gzip) is on both +pages. + +## ranui select: moved off the editor startup + +`index.ts` imported `ranui/select` at top level. Nothing on the editor page +renders an `r-select` until the assistant opens. Every assistant module that +renders one either imports the element itself (`panel.ts`, +`model-configuration-form.ts`) or is imported only by `panel.ts` +(`settings-view.ts`, `speech-input.ts`). The import was removed. The element +now registers when the assistant panel loads, which is the first use. + +The saving is larger than the select chunk alone. ranui's floating controller +and other helpers that only select needed leave the startup graph with it. + +| Page | Before (PR #293) | After | +| ---------- | ------------------------ | ----------------------- | +| `/editor` | 337 KB raw / 105 KB gzip | 271 KB raw / 90 KB gzip | +| `/history` | 198 KB raw / 61 KB gzip | 199 KB raw / 62 KB gzip | + +History does not import `index.ts`, so its bytes stay the same (+0.2 KB raw). +The gzip sum grows by about 1 KB only because Rolldown now emits ranui's button +as a file of its own, and each file is compressed separately. + +## The MIME table: not removable from this repo + +`packages/shared/src/document-utils.ts` was the expected holder, through +`getMimeTypeFromExtension` → `getMime`. That function had no production caller +(the final `rg` returned zero outside `docs/` and an unrelated method of the +same name in the vendored `x2t_helper.js`). Only its unit tests used it, and it +was deleted. + +The table stayed, in both pages. The cause is ranui's packaged +`dist/utils-*.js`, which builds a namespace object over all of ranui's utils, +including its own `getMimeTypeFromExtension` +(`getMime(\`.${ext}\`) || 'application/octet-stream'`). A namespace object +references every member, so a bundler cannot drop any of them. Minimal Vite +builds in this repo confirm it: + +| Entry | MIME table in output | +| -------------------------------------- | -------------------- | +| `import 'ranui/button'` | no | +| `import 'ranui/icon'` / `ranui/modal` | no | +| `import 'ranui/input'` | yes | +| `import 'ranui/message'` | yes | +| `saveFileToDisk` / `createSignal` only | no | + +Both pages need `ranui/input` and `ranui/message`. The editor also calls +`getExtensions(file.type)` from `lib/converter.ts` and `packages/converter` (as +the fallback for a file with no extension), which keeps the table on the editor +regardless of ranui. Removing it from both pages therefore needs a ranui change +first (no namespace object over utils, or ranui's own MIME helper moved out of +it). After that, the editor's two `getExtensions` calls can be replaced with a +lookup derived from `DOCUMENT_MIME_MAP`. Neither is done here. + +## Tests + +`test/e2e/editor-startup-payload.spec.ts`: + +- The editor's startup scripts do not contain a string unique to ranui's select + chunk (`ran-select-dropdown-option`). +- On a fresh editor, `customElements.get('r-select')` is undefined until the + assistant opens. After the settings view opens, it is defined and a select is + visible. + +Reverse validation: with `import 'ranui/select'` back in `index.ts`, both cases +fail (`ranui select is in …/panel-viewport-*.js`, and the element is defined +before the assistant opens). With the import removed, all five cases in the +spec pass. From 0e60c788a8dbae7b6139abfe3bee92832beabf59 Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 20:21:36 +0800 Subject: [PATCH 05/10] perf(editor): name document extensions from the app's own MIME table The open paths fell back to ranuts' getExtensions for a nameless file, which kept its full MIME table on the editor page. getDocumentExtension reverses DOCUMENT_MIME_MAP instead, and names ppt correctly (getExtensions said pot). Co-Authored-By: Claude Opus 5.5 --- lib/converter.ts | 5 ++--- packages/converter/src/document-converter.ts | 6 +++--- packages/shared/README.md | 12 +++++------ packages/shared/src/document-utils.ts | 5 +++++ test/unit/document-utils.test.ts | 21 +++++++++++++++++++- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/lib/converter.ts b/lib/converter.ts index 3c4bdaac..3e9315a0 100644 --- a/lib/converter.ts +++ b/lib/converter.ts @@ -1,8 +1,7 @@ -import { getExtensions } from 'ranuts/utils'; import { t } from '@ranuts/shared/i18n'; import { X2TConverter, isHtmlDocument, preprocessDocxRuby } from '@ranuts/converter'; import { createEditorInstance, loadEditorApi } from './onlyoffice-editor'; -import { getDocumentType } from '@ranuts/shared/document-utils'; +import { getDocumentExtension, getDocumentType } from '@ranuts/shared/document-utils'; // Export types export type { @@ -42,7 +41,7 @@ export async function handleDocumentOperation(options: { await loadEditorApi(); if (options.isCurrent && !options.isCurrent()) throw new DOMException('Document open superseded', 'AbortError'); const { isNew, fileName, file, readonly = false } = options; - const fileType = fileName.split('.').pop() || getExtensions(file?.type || '')[0] || ''; + const fileType = fileName.split('.').pop() || getDocumentExtension(file?.type || ''); const _docType = getDocumentType(fileType); // The type the editor is asked to open as; differs from the file's own // extension only for the CSV case below. diff --git a/packages/converter/src/document-converter.ts b/packages/converter/src/document-converter.ts index 3f252ce5..255989ea 100644 --- a/packages/converter/src/document-converter.ts +++ b/packages/converter/src/document-converter.ts @@ -1,11 +1,11 @@ -import { createObjectURL, getExtensions, isZipContainer, scriptOnLoad } from 'ranuts/utils'; +import { createObjectURL, isZipContainer, scriptOnLoad } from 'ranuts/utils'; import type { BinConversionResult, ConversionResult, DocumentType, EmscriptenModule, } from '@ranuts/shared/document-types'; -import { BASE_PATH, DOCUMENT_TYPE_MAP } from '@ranuts/shared/document-utils'; +import { BASE_PATH, DOCUMENT_TYPE_MAP, getDocumentExtension } from '@ranuts/shared/document-utils'; import { extractDocxMediaUrls } from './docx-zip'; import { MIME_MAP, hasEditorBinSignature, saveFileToDisk } from './file-meta'; import { canStreamWasm, fetchWasmResponse, x2tInstantiateError } from './x2t-loading'; @@ -474,7 +474,7 @@ export class X2TConverter { await this.initialize(); const fileName = file.name; - const fileExt = fileName.split('.').pop() || getExtensions(file?.type)[0] || ''; + const fileExt = fileName.split('.').pop() || getDocumentExtension(file?.type); const documentType = this.getDocumentType(fileExt); try { diff --git a/packages/shared/README.md b/packages/shared/README.md index a8457cd2..3e803865 100644 --- a/packages/shared/README.md +++ b/packages/shared/README.md @@ -24,12 +24,12 @@ import { getDocmentObj, setDocmentObj } from '@ranuts/shared/store'; ## Modules -| Subpath | Exports | -| ------------------ | ------------------------------------------------------------------------------------------------------------------ | -| `./document-utils` | `getDocumentType`, `getDocumentMimeType`, `getBasePath`, `BASE_PATH`, `DOCUMENT_TYPE_MAP`, `parseReadonly` | -| `./document-types` | `DocumentType`, `ConversionResult`, `BinConversionResult`, `SaveEvent`, `EmscriptenModule`, `EmscriptenFileSystem` | -| `./i18n` | `t`, `getLanguage`, `setLanguage`, `getOnlyOfficeLang`, `Language`, `I18nMessages` (zh/en/ja/ko/de/fr/es/pt/ru) | -| `./store` | `getDocmentObj`, `setDocmentObj` (ranuts signal over `{ fileName, file?, url? }`) | +| Subpath | Exports | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| `./document-utils` | `getDocumentType`, `getDocumentMimeType`, `getDocumentExtension`, `getBasePath`, `BASE_PATH`, `DOCUMENT_TYPE_MAP`, `parseReadonly` | +| `./document-types` | `DocumentType`, `ConversionResult`, `BinConversionResult`, `SaveEvent`, `EmscriptenModule`, `EmscriptenFileSystem` | +| `./i18n` | `t`, `getLanguage`, `setLanguage`, `getOnlyOfficeLang`, `Language`, `I18nMessages` (zh/en/ja/ko/de/fr/es/pt/ru) | +| `./store` | `getDocmentObj`, `setDocmentObj` (ranuts signal over `{ fileName, file?, url? }`) | Depends on `ranuts` (getMime, createSignal, cookie/query/localStorage helpers). Builds to `dist/` via `prepare` on install. diff --git a/packages/shared/src/document-utils.ts b/packages/shared/src/document-utils.ts index 554b58c0..7382c794 100644 --- a/packages/shared/src/document-utils.ts +++ b/packages/shared/src/document-utils.ts @@ -66,6 +66,11 @@ export function getDocumentMimeType(fileNameOrExt: string): string { return DOCUMENT_MIME_MAP[ext] || 'application/octet-stream'; } +/** The document extension a MIME type names; '' outside the supported document formats. */ +export function getDocumentExtension(mimeType: string): string { + return Object.keys(DOCUMENT_MIME_MAP).find((ext) => DOCUMENT_MIME_MAP[ext] === mimeType) ?? ''; +} + /** * Document type mapping */ diff --git a/test/unit/document-utils.test.ts b/test/unit/document-utils.test.ts index a8c37269..91139275 100644 --- a/test/unit/document-utils.test.ts +++ b/test/unit/document-utils.test.ts @@ -1,5 +1,12 @@ import { describe, expect, it } from 'vitest'; -import { DOCUMENT_TYPE_MAP, getBasePath, getDocumentType, parseReadonly } from '@ranuts/shared/document-utils'; +import { + DOCUMENT_TYPE_MAP, + getBasePath, + getDocumentExtension, + getDocumentMimeType, + getDocumentType, + parseReadonly, +} from '@ranuts/shared/document-utils'; describe('document utils', () => { it('classifies common document extensions', () => { @@ -21,6 +28,18 @@ describe('document utils', () => { expect(getDocumentType(extension)).toBe(kind); }); + it.each(['docx', 'doc', 'odt', 'rtf', 'txt', 'pdf', 'xlsx', 'xls', 'ods', 'csv', 'pptx', 'ppt', 'odp'])( + 'names %s back from its own MIME type', + (extension) => { + expect(getDocumentExtension(getDocumentMimeType(extension))).toBe(extension); + }, + ); + + it('names no extension for a MIME type outside the document formats', () => { + expect(getDocumentExtension('image/png')).toBe(''); + expect(getDocumentExtension('')).toBe(''); + }); + it('parses the readonly query value for preview mode', () => { // Truthy forms expect(parseReadonly('true')).toBe(true); From 03fc30f463c34ca4b003d8d81c235e26b50ef6cc Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 20:21:37 +0800 Subject: [PATCH 06/10] test(e2e): keep the MIME table out of both startup graphs Co-Authored-By: Claude Opus 5.5 --- test/e2e/editor-startup-payload.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/e2e/editor-startup-payload.spec.ts b/test/e2e/editor-startup-payload.spec.ts index d4c400f0..40af9240 100644 --- a/test/e2e/editor-startup-payload.spec.ts +++ b/test/e2e/editor-startup-payload.spec.ts @@ -15,6 +15,9 @@ const JSCHARDET = { owner: 'jschardet', probe: 'windows-1255' }; // An error the save pipeline (lib/onlyoffice/save-stream.ts) raises: the // history page lists and reopens records, it never saves through the editor. const EDITOR_STACK = { owner: 'editor save pipeline', probe: 'Save conversion failed' }; +// ranuts' full MIME table. ranui stopped holding it in 0.5.2, and the app looks +// MIME types up in its own document table (getDocumentExtension). +const MIME_TABLE = { owner: 'ranuts mimeType table', probe: 'application/vnd.lotus-approach' }; // ranui's select; only the assistant renders one, and it imports the element itself. const RANUI_SELECT = { owner: 'ranui select', probe: 'ran-select-dropdown-option' }; // Two message tables the page does not speak: only English and the active @@ -47,12 +50,12 @@ async function expectAbsent(page: Page, deferred: Array<{ owner: string; probe: test('the editor startup scripts leave optional modules and other locales out', async ({ page }) => { await page.goto('/editor?new=docx'); - await expectAbsent(page, [AGENT_TOOLS, PWA_INSTALL, JSCHARDET, RANUI_SELECT, ...OTHER_LOCALES]); + await expectAbsent(page, [AGENT_TOOLS, PWA_INSTALL, JSCHARDET, MIME_TABLE, RANUI_SELECT, ...OTHER_LOCALES]); }); test('the history page startup scripts leave the editor stack, jschardet and other locales out', async ({ page }) => { await page.goto('/history'); - await expectAbsent(page, [EDITOR_STACK, JSCHARDET, ...OTHER_LOCALES]); + await expectAbsent(page, [EDITOR_STACK, JSCHARDET, MIME_TABLE, ...OTHER_LOCALES]); }); test('a page opened in another language renders that language without reloading', async ({ page }) => { From d99337b4a02b2703a42003982254faf58e74f9c2 Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 20:24:06 +0800 Subject: [PATCH 07/10] docs(perf): record the MIME table fix after the ranui 0.5.2 upgrade Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +- .../2026-10-24-select-and-mime-table.md | 97 +++++++++++++------ 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69d7e48d..41b6e9e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,8 +199,9 @@ notes. Entries describe what users experience, not internal refactors. - The editor and history pages load only the interface language you use rather than all seven, so the editor starts on 105 KB instead of 162 KB compressed, and the history page on 61 KB instead of 121 KB. -- The editor no longer loads the assistant's dropdown control before the - assistant opens (90 KB instead of 105 KB compressed at startup). +- The editor no longer loads the assistant's dropdown control or an unused + file-type table at startup (84 KB instead of 105 KB compressed), and the + history page drops the same table (55 KB instead of 61 KB). - Voice controls stay in place while recording or finishing. Explanations appear without moving the draft, and cancelling preserves the text typed before dictation. - **Configured assistants reopen in chat.** Enable a downloaded model or reconnect diff --git a/docs/explorations/2026-10-24-select-and-mime-table.md b/docs/explorations/2026-10-24-select-and-mime-table.md index 9e4ab686..e1620107 100644 --- a/docs/explorations/2026-10-24-select-and-mime-table.md +++ b/docs/explorations/2026-10-24-select-and-mime-table.md @@ -1,4 +1,4 @@ -# Startup payload, round four: ranui select, and why the MIME table stays +# Startup payload, round four: ranui select and the MIME table Follow-up to `2026-10-24-active-locale-only.md`. After the locale split, two more startup items were found by attributing startup bytes to their source @@ -7,6 +7,22 @@ own and is on the editor page. ranuts' full MIME table (`ranuts/dist/src/utils/mimeType.js`, 30 KB raw / 6.8 KB gzip) is on both pages. +## Result + +The static startup graph, summed per chunk from `.vite/manifest.json` +(gzip -9): + +| Page | Before (main `ee33a4ef`) | After | +| ---------- | ------------------------ | ----------------------- | +| `/editor` | 337 KB raw / 105 KB gzip | 241 KB raw / 84 KB gzip | +| `/history` | 198 KB raw / 61 KB gzip | 169 KB raw / 55 KB gzip | + +Measured step by step on the same base: + +- Removing the select import takes the editor from 105 KB to 90 KB gzip. +- Upgrading ranui to 0.5.2 takes history from 62 KB to 55 KB gzip. +- Replacing `getExtensions` takes the editor from 90 KB to 84 KB gzip. + ## ranui select: moved off the editor startup `index.ts` imported `ranui/select` at top level. Nothing on the editor page @@ -18,32 +34,26 @@ now registers when the assistant panel loads, which is the first use. The saving is larger than the select chunk alone. ranui's floating controller and other helpers that only select needed leave the startup graph with it. +History does not import `index.ts`, so its bytes stay the same. Its gzip sum +still grows by about 1 KB at this step, only because Rolldown now emits ranui's +button as a file of its own, and each file is compressed separately. -| Page | Before (PR #293) | After | -| ---------- | ------------------------ | ----------------------- | -| `/editor` | 337 KB raw / 105 KB gzip | 271 KB raw / 90 KB gzip | -| `/history` | 198 KB raw / 61 KB gzip | 199 KB raw / 62 KB gzip | - -History does not import `index.ts`, so its bytes stay the same (+0.2 KB raw). -The gzip sum grows by about 1 KB only because Rolldown now emits ranui's button -as a file of its own, and each file is compressed separately. - -## The MIME table: not removable from this repo +## The MIME table: held by ranui, then by our own fallback `packages/shared/src/document-utils.ts` was the expected holder, through `getMimeTypeFromExtension` → `getMime`. That function had no production caller (the final `rg` returned zero outside `docs/` and an unrelated method of the same name in the vendored `x2t_helper.js`). Only its unit tests used it, and it -was deleted. +was deleted. That alone saved nothing. -The table stayed, in both pages. The cause is ranui's packaged -`dist/utils-*.js`, which builds a namespace object over all of ranui's utils, -including its own `getMimeTypeFromExtension` +The real holder was in ranui 0.5.1's packaged `dist/utils-*.js`, which built a +namespace object over all of ranui's utils, including an uncalled +`getMimeTypeFromExtension` of its own (`getMime(\`.${ext}\`) || 'application/octet-stream'`). A namespace object references every member, so a bundler cannot drop any of them. Minimal Vite -builds in this repo confirm it: +builds in this repo confirmed it: -| Entry | MIME table in output | +| Entry (ranui 0.5.1) | MIME table in output | | -------------------------------------- | -------------------- | | `import 'ranui/button'` | no | | `import 'ranui/icon'` / `ranui/modal` | no | @@ -51,25 +61,52 @@ builds in this repo confirm it: | `import 'ranui/message'` | yes | | `saveFileToDisk` / `createSignal` only | no | -Both pages need `ranui/input` and `ranui/message`. The editor also calls -`getExtensions(file.type)` from `lib/converter.ts` and `packages/converter` (as -the fallback for a file with no extension), which keeps the table on the editor -regardless of ranui. Removing it from both pages therefore needs a ranui change -first (no namespace object over utils, or ranui's own MIME helper moved out of -it). After that, the editor's two `getExtensions` calls can be replaced with a -lookup derived from `DOCUMENT_MIME_MAP`. Neither is done here. +Both pages need `ranui/input` and `ranui/message`. The function was deleted +upstream in ranui 0.5.2 (published with ranuts 0.5.2 and ranview 0.2.2), which +also adds size budgets for those two entries. This repo upgrades to it. The +minimum-release-age exclusion follows ranview to 0.2.2: the install needs both +versions listed once, because pnpm verifies the old lockfile entry before +resolving, and then a frozen install passes with only 0.2.2. As in the 0.5.1 +upgrade, `public/ran-tokens.css` was re-synced. The IIFEs did not change. + +The editor also kept the table through its own code. `lib/converter.ts` and +`packages/converter` fell back to `getExtensions(file.type)` for a file with +an empty name. Both now call `getDocumentExtension`, which reverses +`DOCUMENT_MIME_MAP` in `@ranuts/shared/document-utils`. For every supported +format it returns the same extension `getExtensions` did, except `ppt`: +`getExtensions` answered `pot`, which is not in `DOCUMENT_TYPE_MAP`. ## Tests `test/e2e/editor-startup-payload.spec.ts`: -- The editor's startup scripts do not contain a string unique to ranui's select - chunk (`ran-select-dropdown-option`). +- The editor's startup scripts contain no string unique to ranui's select chunk + (`ran-select-dropdown-option`). - On a fresh editor, `customElements.get('r-select')` is undefined until the assistant opens. After the settings view opens, it is defined and a select is visible. +- Neither page's startup scripts contain a string unique to the MIME table + (`application/vnd.lotus-approach`). + +`test/unit/document-utils.test.ts`: every supported extension round-trips +through `getDocumentMimeType` → `getDocumentExtension`, and a MIME type outside +the document formats gives `''`. + +Reverse validation, each run against a variant with one fix undone: + +1. With `import 'ranui/select'` back in `index.ts`, both select cases fail + (`ranui select is in …/panel-viewport-*.js`, and the element is defined + before the assistant opens). +2. With `getExtensions` back in `lib/converter.ts`, the editor case fails + (`ranuts mimeType table is in …/document-*.js`). +3. With ranui and ranuts back at 0.5.1, both pages' cases fail + (`ranuts mimeType table is in …/utils-*.js`). + +With all fixes in place: `lint:ts`, `format:check`, all 252 unit files (6817 +tests), and 82 E2E cases pass. The E2E cases cover startup payload, entry +paths, the filename matrix, app smoke, the main site, history, the language +menu, assistant layout and workspace, and the embed API. -Reverse validation: with `import 'ranui/select'` back in `index.ts`, both cases -fail (`ranui select is in …/panel-viewport-*.js`, and the element is defined -before the assistant opens). With the import removed, all five cases in the -spec pass. +One full unit run failed on two tests under machine load: +`agent-artifacts-view` and the 8.9 s `wllama-bounded-file-read`. Both pass on +their own, and they passed in a second full run. From bd8cccc20559eaba0d3e6946754f6c683201f07a Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 20:25:58 +0800 Subject: [PATCH 08/10] docs(shared): correct the package's ranuts and locale notes Co-Authored-By: Claude Opus 5.5 --- packages/shared/CLAUDE.md | 2 +- packages/shared/README.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/shared/CLAUDE.md b/packages/shared/CLAUDE.md index 76eaa541..de1ec7d0 100644 --- a/packages/shared/CLAUDE.md +++ b/packages/shared/CLAUDE.md @@ -23,7 +23,7 @@ import { getDocmentObj, setDocmentObj } from '@ranuts/shared/store'; ## What each module is for - **document-utils** — pure helpers: classify a file (`getDocumentType`), MIME from - extension, the deployment `BASE_PATH` (handles `/document/` on GitHub Pages vs `/`), + extension and back (`getDocumentMimeType` / `getDocumentExtension`), the deployment `BASE_PATH` (handles `/document/` on GitHub Pages vs `/`), and `parseReadonly` for the `?readonly` query flag. - **document-types** — shared interfaces, incl. x2t/Emscripten module shapes. - **i18n** — `t(key)` over the 7 complete shell locales in `SHELL_LOCALES` diff --git a/packages/shared/README.md b/packages/shared/README.md index 3e803865..fd06618c 100644 --- a/packages/shared/README.md +++ b/packages/shared/README.md @@ -24,12 +24,12 @@ import { getDocmentObj, setDocmentObj } from '@ranuts/shared/store'; ## Modules -| Subpath | Exports | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | -| `./document-utils` | `getDocumentType`, `getDocumentMimeType`, `getDocumentExtension`, `getBasePath`, `BASE_PATH`, `DOCUMENT_TYPE_MAP`, `parseReadonly` | -| `./document-types` | `DocumentType`, `ConversionResult`, `BinConversionResult`, `SaveEvent`, `EmscriptenModule`, `EmscriptenFileSystem` | -| `./i18n` | `t`, `getLanguage`, `setLanguage`, `getOnlyOfficeLang`, `Language`, `I18nMessages` (zh/en/ja/ko/de/fr/es/pt/ru) | -| `./store` | `getDocmentObj`, `setDocmentObj` (ranuts signal over `{ fileName, file?, url? }`) | - -Depends on `ranuts` (getMime, createSignal, cookie/query/localStorage helpers). +| Subpath | Exports | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `./document-utils` | `getDocumentType`, `getDocumentMimeType`, `getDocumentExtension`, `getBasePath`, `BASE_PATH`, `DOCUMENT_TYPE_MAP`, `parseReadonly` | +| `./document-types` | `DocumentType`, `ConversionResult`, `BinConversionResult`, `SaveEvent`, `EmscriptenModule`, `EmscriptenFileSystem` | +| `./i18n` | `t`, `getLanguage`, `setLanguage`, `getOnlyOfficeLang`, `Language`, `I18nMessages` (en/zh-CN/ja/ko/de/es/pt; non-English tables load on demand) | +| `./store` | `getDocmentObj`, `setDocmentObj` (ranuts signal over `{ fileName, file?, url? }`) | + +Depends on `ranuts` (createSignal, cookie/query/localStorage helpers). Builds to `dist/` via `prepare` on install. From e1612c948c14a0bdafaa50c4d28940e6dbcad1c4 Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 21:29:06 +0800 Subject: [PATCH 09/10] test(e2e): open assistant settings without racing the cache check Co-Authored-By: Claude Opus 5.5 --- .../2026-10-24-select-and-mime-table.md | 15 +++++++++++++++ test/e2e/editor-startup-payload.spec.ts | 8 +++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/explorations/2026-10-24-select-and-mime-table.md b/docs/explorations/2026-10-24-select-and-mime-table.md index e1620107..2703ef1b 100644 --- a/docs/explorations/2026-10-24-select-and-mime-table.md +++ b/docs/explorations/2026-10-24-select-and-mime-table.md @@ -110,3 +110,18 @@ menu, assistant layout and workspace, and the embed API. One full unit run failed on two tests under machine load: `agent-artifacts-view` and the 8.9 s `wllama-bounded-file-read`. Both pass on their own, and they passed in a second full run. + +## CI race in the select case + +The first CI run failed this case on `E2E (Docker image) shard 1`, with no +visible `r-select` in settings. With no saved model, the panel checks the local +model cache asynchronously and switches itself to settings when nothing is +cached (`lib/agent-plugin/ui/panel.ts`, the `externalEntry` block at the end). +The gear button toggles. When the check finished before the click, the click +toggled the panel back to chat. Forcing that ordering locally (waiting for +`data-view="settings"` before the click) reproduces the CI error exactly. + +The case now waits for the cache summary to settle, then presses the gear only +when the panel is not already in settings, as `agent-portal-layer.spec.ts` +does. With the same forced ordering it passes, and 40 repeated runs of the spec +pass. diff --git a/test/e2e/editor-startup-payload.spec.ts b/test/e2e/editor-startup-payload.spec.ts index 40af9240..b6aecbfa 100644 --- a/test/e2e/editor-startup-payload.spec.ts +++ b/test/e2e/editor-startup-payload.spec.ts @@ -78,7 +78,13 @@ test('the select element is defined once the assistant opens, without being in s await page.waitForFunction(() => Boolean(window.__ooFrames.readyEditor())); expect(await page.evaluate(() => Boolean(customElements.get('r-select')))).toBe(false); await page.frameLocator('iframe[name="frameEditor"]').locator('.agent-sidebar-entry').click(); - await page.locator('.agent-panel-settings-toggle').click(); + // With no saved model the panel checks the model cache and may switch to + // settings by itself; the gear toggles, so only press it when it would open them. + const panel = page.locator('.agent-runtime-panel'); + await expect(page.locator('.agent-model-cache > summary')).toContainText('(0)'); + if ((await panel.getAttribute('data-view')) !== 'settings') + await page.locator('.agent-panel-settings-toggle').click(); + await expect(panel).toHaveAttribute('data-view', 'settings'); await expect.poll(() => page.evaluate(() => Boolean(customElements.get('r-select')))).toBe(true); await expect(page.locator('.agent-panel-settings r-select:visible').first()).toBeVisible(); }); From bbc2738a355d7cf2930a398c76bf240366bdb242 Mon Sep 17 00:00:00 2001 From: chaxus Date: Sat, 24 Oct 2026 21:56:31 +0800 Subject: [PATCH 10/10] docs(perf): restate round four on top of the ranui 0.5.2 upgrade Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +-- .../2026-10-24-select-and-mime-table.md | 15 ++++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b6e9e4..dabf6ab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -200,8 +200,7 @@ notes. Entries describe what users experience, not internal refactors. than all seven, so the editor starts on 105 KB instead of 162 KB compressed, and the history page on 61 KB instead of 121 KB. - The editor no longer loads the assistant's dropdown control or an unused - file-type table at startup (84 KB instead of 105 KB compressed), and the - history page drops the same table (55 KB instead of 61 KB). + file-type table at startup (84 KB instead of 105 KB compressed). - Voice controls stay in place while recording or finishing. Explanations appear without moving the draft, and cancelling preserves the text typed before dictation. - **Configured assistants reopen in chat.** Enable a downloaded model or reconnect diff --git a/docs/explorations/2026-10-24-select-and-mime-table.md b/docs/explorations/2026-10-24-select-and-mime-table.md index 2703ef1b..c3a0c95f 100644 --- a/docs/explorations/2026-10-24-select-and-mime-table.md +++ b/docs/explorations/2026-10-24-select-and-mime-table.md @@ -12,15 +12,16 @@ pages. The static startup graph, summed per chunk from `.vite/manifest.json` (gzip -9): -| Page | Before (main `ee33a4ef`) | After | +| Page | Before (main `75c2cd96`) | After | | ---------- | ------------------------ | ----------------------- | | `/editor` | 337 KB raw / 105 KB gzip | 241 KB raw / 84 KB gzip | -| `/history` | 198 KB raw / 61 KB gzip | 169 KB raw / 55 KB gzip | +| `/history` | 168 KB raw / 55 KB gzip | 169 KB raw / 56 KB gzip | -Measured step by step on the same base: +The ranui 0.5.2 upgrade landed separately in #294. Measured on #293's main +(`ee33a4ef`), it is what took history from 62 KB to 55 KB gzip. On top of it, +this change moves only the editor: - Removing the select import takes the editor from 105 KB to 90 KB gzip. -- Upgrading ranui to 0.5.2 takes history from 62 KB to 55 KB gzip. - Replacing `getExtensions` takes the editor from 90 KB to 84 KB gzip. ## ranui select: moved off the editor startup @@ -63,11 +64,7 @@ builds in this repo confirmed it: Both pages need `ranui/input` and `ranui/message`. The function was deleted upstream in ranui 0.5.2 (published with ranuts 0.5.2 and ranview 0.2.2), which -also adds size budgets for those two entries. This repo upgrades to it. The -minimum-release-age exclusion follows ranview to 0.2.2: the install needs both -versions listed once, because pnpm verifies the old lockfile entry before -resolving, and then a frozen install passes with only 0.2.2. As in the 0.5.1 -upgrade, `public/ran-tokens.css` was re-synced. The IIFEs did not change. +also adds size budgets for those two entries. This repo picked it up in #294. The editor also kept the table through its own code. `lib/converter.ts` and `packages/converter` fell back to `getExtensions(file.type)` for a file with