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
onclickhandlers call ~25 named globals. These must stay onwindow. No renaming. - Load order via
<script>tags — dependencies load first. - Service worker —
sw.jspre-caches the file list; must be updated. - Cache busting — all
<script>tags use?v=%%APP_VERSION%%. - Docker entrypoint —
COPY src/covers everything. No changes needed. - CI syntax check — already runs
node --checkonsrc/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 --checkvalidation 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 inindex.htmlbeforeui.js - Add to
sw.jsSHELL_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 afterui.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 afterui.js - Add to
sw.js - Run syntax check + tests
5. Extract tokens.js
- Create
src/js/tokens.js - Move
Tokensobject +updateInputTokens+updateContextWarning+dismissContextWarning - Remove from
app.js - Add
<script>tag beforeapp.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()frominitListeners()— cut the notes-related event bindings into a new function, call it frominitListeners() - Add
<script>tag beforeapp.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()frominitListeners()— cut chat input, Enter-to-send, message action delegation, etc. - Add
<script>tag beforeapp.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()frominitListeners() - Add
<script>tag beforeapp.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()frominitListeners() - Add
<script>tag beforeapp.js - Add to
sw.js - Run syntax check + tests
10. Final validation
node --checkall JS filesnode --test src/js/__tests__/*.test.js— all 159 tests pass- Verify
index.htmlscript order matches dependency graph - Verify
sw.jsSHELL_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
UIobject is still a single global — just defined across files - All tests continue to work unchanged