This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/REFACTOR-0.10.3.md
2026-02-26 21:19:55 +00:00

15 KiB

v0.10.3 — Frontend Refactor Plan

Problem

Two monolith files accumulate every feature:

File Lines Role
app.js 2,940 All application logic + listeners
ui.js 2,582 All rendering + admin + settings
api.js 575 HTTP client (fine)
debug.js 580 Debug logger (fine)
events.js 327 EventBus WebSocket (fine)
Total 7,004

The v0.10.2 syntax bug (orphan brace nesting everything from notes through banners) is a direct consequence: in a 2,940-line file, a misplaced insertion is invisible.

Constraints

  • Vanilla JS — no build step, no bundler, no ES modules.
  • Global functions — HTML onclick handlers call ~25 named globals. These must stay on window. No renaming.
  • Load order via <script> tags — dependencies load first.
  • Service workersw.js pre-caches the file list; must be updated.
  • Cache busting — all <script> tags use ?v=%%APP_VERSION%%.
  • Docker entrypointCOPY src/ covers everything. No changes needed.
  • CI syntax check — already runs node --check on src/js/*.js.

Architecture

The UI object stays as a single global with methods. Domain files extend it via Object.assign(UI, { ... }). The App state object stays in app.js. All functions stay global.

Load order (dependency graph, top to bottom):

vendor/marked.min.js, vendor/purify.min.js
  ↓
debug.js, events.js
  ↓
api.js
  ↓
ui-format.js          ← esc(), markdown rendering (used by everything)
  ↓
ui-core.js            ← UI object: sidebar, chat list, messages, streaming, model selector
  ↓
ui-settings.js        ← Object.assign(UI, { settings/team methods })
ui-admin.js           ← Object.assign(UI, { admin modal methods })
  ↓
tokens.js             ← Tokens object, context tracking
notes.js              ← Notes panel
chat.js               ← Chat ops, send, regen, edit, branch, summarize
settings-handlers.js  ← Provider CRUD, command palette, save handlers
admin-handlers.js     ← Admin actions, presets, team management
  ↓
app.js                ← State, init, boot, auth, listeners (orchestrator)

File Breakdown

Unchanged (3 files, 1,482 lines)

File Lines Notes
api.js 575 Clean. Self-contained HTTP client.
debug.js 580 Clean. Debug logger + modal.
events.js 327 Clean. EventBus WebSocket.

From ui.js → 4 files

ui-format.js (~280 lines)

Extract from ui.js bottom section + helpers.

Formatting (L2241-2462):
  _renderMarkdown(text)
  _renderCodeBlocks(html)
  _copyCodeBlock(btn)
  _formatTokenCount(n)
  _formatCost(c)

Helpers (L2519-2583):
  esc(s)                    ← global function, used everywhere
  _formatTimestamp(ts)
  _formatRelativeTime(ts)
  _truncate(s, n)
  openSidePanel(panelId)    ← global, called from notes/search
  closeSidePanel()          ← global, HTML onclick

Side Panel Resize (L2463-2518):
  _initSidePanelResize()

Why separate: Formatting is a pure utility layer. Every other UI file depends on esc() and _renderMarkdown(). Loading first eliminates forward references.

ui-core.js (~650 lines)

The UI object definition with core rendering methods.

const UI = {
  // Sidebar (L189-216)
  toggleSidebar(), restoreSidebar(),
  toggleUserMenu(), closeUserMenu(),

  // Chat List (L217-284)
  renderChatList(),

  // Messages (L285-454)
  renderMessages(), showEmptyState(),
  _messageHTML(), _summaryHTML(), _isSummaryMessage(),
  toggleSummarizedHistory(),

  // Streaming (L455-596)
  streamResponse(),

  // Model Selector (L597-727)
  getModelValue(), setModelValue(),
  updateModelSelector(), initModelDropdown(),

  // Capability Badges (L728-774)
  getSelectedModelCaps(), updateCapabilityBadges(),

  // User / Connection (L775-811)
  updateUser(), showAdminButton(),

  // Generating State (L812-862)
  setGenerating(), showRegenerate(),

  // Toast (L863-873)
  toast(),

  // Settings Modal (L874-921) — thin dispatcher
  openSettings(), switchSettingsTab(),

  // Export (L2191-2214)
  exportMarkdown(), exportJSON(),

  // Message Actions (L2215-2240)
  (delegated action handlers)

  // Scroll (L2226-2240)
  scrollToBottom(),
};

Also includes the top-level _avatarHTML() helper (L5-24) and summary message helpers (L167-188).

ui-settings.js (~550 lines)

Extends UI with all settings modal tab content.

Object.assign(UI, {
  // Preset Form Component (L25-166) — _presetFormHTML(), etc.

  // Appearance (L922-1267)
  loadAppearanceSettings(), initAppearance(),

  // Providers (L1444-1480)
  loadProviderList(), hideProviderForm(),

  // Profile (L994-1002)
  loadProfileIntoSettings(),

  // Teams (L1003-1267)
  loadMyTeams(), loadTeamsTab(), openTeamManage(),
  showTeamPicker(), loadTeamManageMembers(),
  loadTeamManageProviders(), loadTeamManagePresets(),
  loadTeamAuditLog(), loadTeamPresetModelDropdown(),
  switchTeamTab(),

  // Usage (L1160-1267)
  loadMyUsage(), loadTeamUsage(),

  // User Model Roles (L1268-1328)
  loadUserRoles(),

  // User Models & Presets (L2118-2190)
  loadUserModels(), loadUserPresets(),

  // checkUserProvidersAllowed(), checkUserPresetsAllowed()
});

ui-admin.js (~650 lines)

Extends UI with all admin modal content.

Object.assign(UI, {
  // Admin Modal (L1481-1604)
  openTeamAdmin(), switchAdminTab(),

  // Users (L1549-1588)
  loadAdminUsers(),

  // Stats (L1589-1604)
  loadAdminStats(),

  // Roles (L1605-1658)
  loadAdminRoles(),

  // Usage + Audit (L1659-1813)
  loadAdminUsage(), loadAuditLog(),

  // Providers (L1814-1835)
  loadAdminProviders(),

  // Models (L1836-1863)
  loadAdminModels(),

  // Presets (L1864-1939)
  loadAdminPresets(),

  // Teams (L1940-2117)
  loadAdminTeams(), openTeamDetail(),
  loadTeamMembers(), loadMemberUserDropdown(),

  // Settings (L2037-2117)
  loadAdminSettings(), updateBannerPreview(),
});

From app.js → 6 files

tokens.js (~120 lines)

Already has its own Tokens namespace — clean extraction.

const Tokens = { ... };  // L36-74
function updateInputTokens()      // L78-106
function updateContextWarning()   // L108-147
function dismissContextWarning()  // L148-152

notes.js (~300 lines)

Self-contained with own state variables.

var _editingNoteId = null;
var _notesSelectMode = false;
var _selectedNoteIds = new Set();
var _notesSort = 'updated_desc';

openNotes(), loadNotesList(), _noteListItem()
_highlightHeadline()
_enterSelectMode(), _exitSelectMode()
_toggleNoteSelect(), _toggleSelectAll()
_updateSelectedCount(), _bulkDeleteSelected()
loadNoteFolders(), showNotesList()
copyNoteContent(), openNoteEditor()
_populateEditFields(), _clearEditFields()
_showNoteReadMode(), _showNoteEditMode()
_showNotePreview()
saveNote(), deleteNote()
_initNotesListeners()   ← NEW: extracted from initListeners

chat.js (~500 lines)

All conversation operations.

// Chat management
loadChats(), selectChat(), newChat(), deleteChat()

// Per-chat model persistence
_saveChatModel(), _restoreChatModel()

// Send + stream
sendMessage(), stopGeneration()

// Active path
reloadActivePath()

// Regenerate
regenerateMessage(), regenerate()

// Edit
editMessage(), submitEdit(), cancelEdit()

// Branch navigation
switchSibling()

// Summarize & Continue
summarizeAndContinue()

// Preview
clearPreview()

_initChatListeners()   ← NEW: extracted from initListeners

settings-handlers.js (~400 lines)

Settings-related handlers + command palette.

// Settings
handleSaveSettings(), loadSettings(), saveSettings()

// Avatar
updateAvatarPreview()

// Providers
handleCreateProvider(), deleteProvider()
refreshProviderModels(), editProvider()

// Admin settings save
handleSaveAdminSettings()

// User model roles
userRoleProviderChanged(), saveUserRole(), clearUserRole()

// Command Palette
_cmdCommands[], toggleCmdPalette(), openCmdPalette()
closeCmdPalette(), _handleCmdKey(), _highlightCmdItem()
_getVisibleCommands(), _renderCmdResults(), executeCmdAction()

_initSettingsListeners()  ← NEW: extracted from initListeners

admin-handlers.js (~450 lines)

Admin actions + team management.

// Admin user management
toggleUserActive(), showApproveForm(), hideApproveForm()
handleApproveUser(), resetUserPassword(), promoteUser()
createAdminUser()

// Admin roles
adminSaveRole(), adminRoleProviderChanged()

// User model preferences
toggleModelVisibility(), bulkSetVisibility()
bulkSetUserModelVisibility(), deleteUserPreset()

// Admin presets
_adminPresetForm, ensureAdminPresetForm()
editAdminPreset(), createAdminPreset()
toggleAdminPreset(), deleteAdminPreset()

// Team admin (system admin)
toggleTeamActive(), deleteTeam()
updateTeamMember(), removeTeamMember()

// Team admin (settings-side)
settingsUpdateTeamMember(), settingsRemoveTeamMember()
settingsDeleteTeamPreset(), settingsToggleTeamProvider()
settingsDeleteTeamProvider(), settingsEditTeamProvider()

_initAdminListeners()  ← NEW: extracted from initListeners

app.js (~400 lines) — the orchestrator

What remains after extraction.

// State
const App = { ... };

// Models
resolveCapabilities(), fetchModels()

// Init + Boot
init(), startApp()

// Auth flow
showSplash(), hideSplash()
handleLogin(), handleRegister(), handleLogout()
switchAuthTab(), setAuthError(), setAuthLoading()

// Modal helpers
openModal(), closeModal()
checkTabsOverflow(), updateTabArrows()

// Branding + Banners
initBranding(), initBanners()

// Listeners — thin dispatcher
function initListeners() {
    if (_listenersInit) return;
    _listenersInit = true;
    _initChatListeners();       // from chat.js
    _initSettingsListeners();   // from settings-handlers.js
    _initAdminListeners();      // from admin-handlers.js
    _initNotesListeners();      // from notes.js
    _initGlobalKeyboard();      // local: Escape, Ctrl+K, resize
    _initSidePanelResize();     // from ui-format.js
}

// Boot
document.addEventListener('DOMContentLoaded', init);

Execution Steps

1. Preparation (before any splits)

  • Branch from merged v0.10.2
  • Add node --check validation to a quick local script: for f in src/js/*.js; do node --check "$f" || exit 1; done
  • Run all frontend tests as baseline: node --test src/js/__tests__/*.test.js

2. Extract ui-format.js

  • Create src/js/ui-format.js
  • Move: esc(), formatting functions, side panel resize, helpers
  • Remove from ui.js
  • Add <script> tag in index.html before ui.js
  • Add to sw.js SHELL_FILES
  • Run syntax check + tests

3. Extract ui-settings.js

  • Create src/js/ui-settings.js
  • Move settings/team UI methods out of UI object in ui.js
  • Add Object.assign(UI, { ... }) wrapper
  • Add <script> tag after ui.js
  • Add to sw.js
  • Run syntax check + tests

4. Extract ui-admin.js

  • Create src/js/ui-admin.js
  • Move admin UI methods out of UI object
  • Add Object.assign(UI, { ... }) wrapper
  • Add <script> tag after ui.js
  • Add to sw.js
  • Run syntax check + tests

5. Extract tokens.js

  • Create src/js/tokens.js
  • Move Tokens object + updateInputTokens + updateContextWarning + dismissContextWarning
  • Remove from app.js
  • Add <script> tag before app.js
  • Add to sw.js
  • Run syntax check + tests

6. Extract notes.js

  • Create src/js/notes.js
  • Move all notes state + functions
  • Extract _initNotesListeners() from initListeners() — cut the notes-related event bindings into a new function, call it from initListeners()
  • Add <script> tag before app.js
  • Add to sw.js
  • Run syntax check + tests

7. Extract chat.js

  • Create src/js/chat.js
  • Move chat management, send, regen, edit, branch, summarize, model persistence
  • Extract _initChatListeners() from initListeners() — cut chat input, Enter-to-send, message action delegation, etc.
  • Add <script> tag before app.js
  • Add to sw.js
  • Run syntax check + tests

8. Extract settings-handlers.js

  • Create src/js/settings-handlers.js
  • Move settings handlers, provider CRUD, command palette, avatar, user roles
  • Extract _initSettingsListeners() from initListeners()
  • Add <script> tag before app.js
  • Add to sw.js
  • Run syntax check + tests

9. Extract admin-handlers.js

  • Create src/js/admin-handlers.js
  • Move admin actions, presets, team management
  • Extract _initAdminListeners() from initListeners()
  • Add <script> tag before app.js
  • Add to sw.js
  • Run syntax check + tests

10. Final validation

  • node --check all JS files
  • node --test src/js/__tests__/*.test.js — all 159 tests pass
  • Verify index.html script order matches dependency graph
  • Verify sw.js SHELL_FILES lists all new files
  • Manual smoke test: login, chat, settings, admin, notes, debug
  • Grep for any remaining forward references / undefined calls
  • Update ROADMAP: mark v0.10.3 complete
  • Update CHANGELOG

Target State

File Lines Domain
api.js 575 HTTP client, auth tokens
debug.js 580 Debug logger + modal
events.js 327 EventBus WebSocket
ui-format.js ~280 Markdown, code blocks, esc(), helpers
ui-core.js ~650 UI object: rendering, model selector, streaming
ui-settings.js ~550 Settings tabs, teams, providers, user prefs
ui-admin.js ~650 Admin tabs, users, roles, usage, teams
tokens.js ~120 Context tracking, token estimation
notes.js ~300 Notes panel, editor, multi-select
chat.js ~500 Chat ops, send, regen, edit, branch, summarize
settings-handlers.js ~400 Settings save, provider CRUD, command palette
admin-handlers.js ~450 Admin actions, presets, team management
app.js ~400 State, init, boot, auth, listener dispatch
Total ~5,782 13 files (was 5)

Line count drops slightly due to removed duplicate section headers and consolidated comments. Average file: ~445 lines. Largest: ~650.

Risk Mitigation

Forward references: Function declarations are hoisted within a <script>, but not across scripts. All functions called by initListeners() must be defined in scripts loaded before app.js. The load order above ensures this.

UI object extension: Object.assign(UI, { ... }) in ui-settings.js and ui-admin.js means UI must be defined first (in ui-core.js). The load order handles this.

Regression testing: Each extraction step ends with syntax check + test run. If anything breaks, it's isolated to the last extraction.

Rollback: Each step is a single commit. Revert any step independently.

What This Does NOT Change

  • No ES modules, no import/export, no bundler
  • No function renaming (HTML onclick handlers untouched)
  • No new dependencies
  • No architectural changes to backend
  • No feature additions
  • The UI object is still a single global — just defined across files
  • All tests continue to work unchanged