Summary
Refactor src/ui/app.ts from a large application controller/service locator into a small composition root plus explicit, constructible application services and route-scoped sessions.
This is an architectural refactor. Preserve current behavior, build output, keyboard shortcuts, UI flows, persistence formats, authentication behavior, query semantics, cancellation semantics, and test coverage. Do not redesign the UI or introduce a framework as part of this issue.
The target is a modular monolith with clear boundaries for Workbench, Dashboard, future dashboard persistence, and an AI/MCP helper.
Why
src/ui/app.ts currently owns or coordinates all of the following:
- OAuth and Basic authentication
- ClickHouse connection context
- query execution and script execution
- parameter preparation and execution gating
- schema/reference loading
- editor integration and Spec validation
- query history and saved-query persistence
- single-query and script export
- schema lineage graphs
- sharing
- dashboard bootstrap and auth handoff
- route DOM construction
- action registration and reactive effects
The file is difficult to change safely because unrelated features share the same mutable App object and global AppState. Future product blocks—Workbench, Dashboard, persisted dashboard documents, share links, and an AI helper—need reusable application capabilities that do not depend on DOM nodes or the full App contract.
Architectural rules
Every extraction in this issue must follow these rules:
- Extracted application services must not accept
App or AppState as constructor arguments.
- Dependencies must be explicit and narrow interfaces.
- Non-view services must not import or call DOM helpers,
renderResults, renderApp, flashToast, editor adapters, or UI modules.
- Services return typed results, throw typed/domain errors where appropriate, or publish typed state/events. The UI decides how to render messages.
- Cancellation state belongs to the session or operation that owns it.
- Route-specific state must not be added back to the global
AppState as a shortcut.
- Existing core functions should be reused rather than wrapped in generic abstraction layers.
- Keep the application framework-free. This issue does not introduce React, Preact, Vue, or a generic dependency-injection container.
- Preserve the existing single-file production build. Code splitting is a separate concern.
- Each migration step must leave the application runnable and tests green.
Target module layout
Names may be adjusted slightly when implementation reveals a better fit, but preserve these responsibilities and boundaries.
src/
application/
connection-session.ts
query-execution-service.ts
export-service.ts
schema-catalog-service.ts
workbench-parameter-session.ts
query-document-session.ts
saved-query-service.ts
schema-graph-session.ts
app-preferences.ts
errors.ts
ui/
workbench/
workbench-session.ts
workbench-shell.ts
workbench-actions.ts
dashboard/
dashboard-session.ts
dashboard-shell.ts
app.ts # composition/bootstrap only
Existing pure modules under src/core, network code under src/net, editor ports, and focused UI render modules should remain where they are unless moving one is required to break an actual dependency cycle.
Required extractions
1. QueryExecutionService
Extract the query transport and execution policy that currently exists through runReadInto, run, runScript, query IDs, ClickHouse sessions, retry classification, parameter transport, and cancellation.
The service must be constructible without the UI and usable by:
- Workbench single-statement execution
- Workbench script execution
- Dashboard tile execution
- detached data views
- future AI/MCP commands that execute approved queries
Suggested API shape:
export interface QueryExecutionService {
executeRead(request: ReadExecutionRequest): Promise<ReadExecutionResult>;
executeScript(request: ScriptExecutionRequest): Promise<ScriptExecutionResult>;
cancel(operationId: string): Promise<void>;
}
Required behavior to preserve:
- streamed structured results
- explicit
FORMAT/raw response handling
- row limits
- query IDs and server-side
KILL QUERY
- abort behavior preserving partial streamed results
- per-tab ClickHouse HTTP session semantics for
TEMPORARY and SET
- script stop-on-first-failure behavior
- retry only for
SESSION_IS_LOCKED, or transient read-only statements where currently allowed
- warning for ambiguous network failure after non-idempotent statements
- schema-mutating result classification exposed to the caller
Do not make this service write into tabs, history, dashboard tiles, DOM, or global signals.
2. ConnectionSession
Extract authentication and ClickHouse connection lifecycle:
- config and IdP loading/memoization
- OAuth login, token exchange/refresh inputs, token storage hooks
- Basic-auth connection probing
- target origin
- auth-header construction
- current identity
- sign-in/sign-out
ensureAuthenticated
- construction of the ClickHouse request context
- cross-tab auth snapshot import/export primitives
Suggested API:
export interface ConnectionSession {
readonly state: ReadonlySignal<ConnectionState>;
ensureAuthenticated(): Promise<boolean>;
beginOAuth(input?: OAuthLoginInput): Promise<void>;
connectBasic(input: BasicConnectionInput): Promise<void>;
signOut(): void;
identity(): ConnectionIdentity | null;
createClickHouseContext(): ChCtx;
snapshotAuth(): AuthSnapshot | null;
restoreAuth(snapshot: AuthSnapshot): void;
}
The service must not render the login page or toast. Authentication failures must be represented as typed state or errors for the shell to handle.
3. WorkbenchSession
Create a route-scoped Workbench session that owns:
- open query documents/tabs
- active document
- Workbench running operation
- Workbench cancellation state
- result view and sort state
- editor mode coordination
- Workbench parameter session
- Workbench-only selection state
- lifecycle cleanup
It may use signals internally. It must expose a narrow interface/view state to the Workbench shell.
It must provide destroy() and release:
- reactive effects created by the session
- timers
- event listeners
- in-flight Workbench operations
Do not move Dashboard state into this session.
4. DashboardSession
Create a separate route-scoped Dashboard session that owns:
- dashboard loading/runtime state
- tile execution queue and concurrency
- per-tile cancellation state
- filters and filter execution state
- dashboard-specific errors and progress
- dashboard lifecycle cleanup
The Dashboard session must depend on QueryExecutionService and dashboard/query repositories through narrow interfaces. It must not depend on WorkbenchSession, editor ports, Workbench DOM, or the complete App object.
It must provide destroy() and cancel all in-flight tile/filter work.
This issue does not require persistent DashboardDocument v1, but the session API must not assume that a dashboard is permanently derived from the favorites list. Accept a dashboard runtime input/model through an explicit interface so a stored document can replace the current source later.
5. ExportService
Extract:
- direct query export
- script export
- streaming to disk
- hold-back inspection for ClickHouse exception frames
- partial-file behavior
- file/directory naming
- progress state
- export-specific query IDs and cancellation
Separate application policy from browser filesystem access:
export interface ExportSink {
pickFile(input: PickFileInput): Promise<FileTarget | null>;
pickDirectory(input: PickDirectoryInput): Promise<DirectoryTarget | null>;
}
ExportService must be testable using an in-memory sink. It may depend on query/connection primitives, but must not use Workbench DOM or mutate AppState directly.
6. SchemaCatalogService
Extract server metadata and reference-data lifecycle:
- server version
- schema loading
- lazy column loading
- reference keywords/functions
- completion snapshot construction
- entity documentation cache
- cache invalidation when the connection changes
Suggested API:
export interface SchemaCatalogService {
loadVersion(): Promise<string>;
refreshSchema(): Promise<SchemaCatalog>;
loadColumns(ref: TableRef): Promise<SchemaColumn[]>;
loadReferenceData(): Promise<ReferenceData>;
getEntityDocumentation(name: string): Promise<string | null>;
completionSnapshot(): CompletionSnapshot;
invalidate(): void;
}
Keep rendering of the schema tree and connection banner in UI modules.
7. WorkbenchParameterSession
Extract Workbench parameter state and policy currently mixed into renderVarStrip and query execution:
- parameter analysis
- value and optional-block activation state
- input-mode versus execute-mode validation
- hardened invalid values
- schema-inferred enum suggestions
- recent-value recording and clearing
- execution preparation snapshot
- Run eligibility/gate result
Suggested API:
export interface WorkbenchParameterSession {
analyze(sql: string): ParameterViewModel[];
updateValue(name: string, value: string): void;
commitValue(name: string): void;
prepare(sql: string, wallNowMs: number): PreparedSource;
gate(sql: string, mode: 'input' | 'execute'): ExecutionGate;
recordBoundParams(params: readonly BoundParamSnapshot[]): void;
}
The session returns view models. A focused VarStripView remains responsible for DOM controls, focus preservation, combobox behavior, and painting diagnostics.
8. QueryDocumentSession and SavedQueryService
Separate an open document from its persisted representation.
QueryDocumentSession owns:
- SQL draft
- Spec text
- parsed Spec and diagnostics
- dirty flags
- editor mode
- linked saved-query ID
- last successful result columns
- optional ClickHouse HTTP session ID
SavedQueryService owns:
- create/update saved query
- validation before persistence
- history recording
- share serialization for the current URL format
- repository/storage interaction
Do not let saved-query persistence call editor or rendering methods. The Workbench session coordinates persistence results with its document state.
9. SchemaGraphSession
Extract schema-lineage operation state:
- progressive base/progress/final graph updates
- stale-operation protection
- abort/cancel behavior
- expanded graph loading
- table detail loading
- saved node positions
- latest-detail-request protection
Suggested API:
export interface SchemaGraphSession {
load(focus: SchemaFocus): AsyncIterable<SchemaGraphUpdate>;
expand(focus: SchemaFocus): Promise<ExpandedSchemaGraph>;
loadNodeDetail(node: SchemaNodeRef): Promise<NodeDetail>;
cancel(): void;
destroy(): void;
}
The service/session returns graph models. Opening a child window/overlay and drawing SVG remain UI adapter responsibilities.
10. AppPreferences
Centralize typed access to persisted user preferences and local/session storage keys.
At minimum include:
- theme and density
- layout dimensions
- result-row limit
- variable values/activation/recents
- library metadata currently treated as preferences
Do not force saved-query/library domain records into a generic key-value API if a dedicated repository is clearer.
app.ts target responsibility
At completion, src/ui/app.ts should be a composition/bootstrap module. It may:
- adapt the browser environment
- construct repositories, ports, sessions, and services
- choose Workbench or Dashboard route
- mount the relevant shell
- coordinate initial authentication routing
- expose a small top-level
Application lifecycle
It should not contain business workflows such as query execution, export streaming, schema graph loading, saved-query commits, parameter validation, or authentication refresh logic.
Illustrative target:
export function createApplication(env: AppEnvironment): Application {
const preferences = createAppPreferences(env.storage);
const connection = createConnectionSession({ env, preferences });
const execution = createQueryExecutionService({ connection, clock: env.clock });
const catalog = createSchemaCatalogService({ execution, connection });
const exports = createExportService({ execution, connection, sink: env.exportSink });
const savedQueries = createSavedQueryService({ repository: env.savedQueryRepository });
return {
mountWorkbench(root) {
const session = createWorkbenchSession({
execution,
connection,
catalog,
exports,
savedQueries,
preferences,
});
return mountWorkbenchShell(root, session);
},
mountDashboard(root, input) {
const session = createDashboardSession({ execution, connection, input });
return mountDashboardShell(root, session);
},
};
}
The final implementation does not need to match this code literally, but it must produce the same dependency direction.
Migration plan
Implement as incremental PRs or commits. Do not rewrite app.ts in one pass.
Phase 0: characterization and dependency map
- Add or identify characterization tests for critical current workflows.
- Record the current
app.ts line count and App interface member count in the PR description, not as a permanent test.
- Identify every import of
App, AppState, runReadInto, auth helpers, and route-specific state.
- Add no architecture-only abstractions in this phase.
Required characterization coverage:
- single read execution and streaming
- explicit
FORMAT
- cancellation and partial result
- script execution/retry/stop-on-error
- Basic and OAuth token refresh paths
- dashboard tile execution
- direct and script export cancellation
- schema graph stale-write protection
- save/share validation
Phase 1: QueryExecutionService
- Extract transport and execution state first.
- Keep thin compatibility wrappers on
app temporarily where needed.
- Migrate Workbench, Dashboard, and detached data execution to the service.
- Delete
app.runReadInto after all callers migrate.
Phase 2: ConnectionSession
- Extract auth/config/ClickHouse context.
- Migrate login UI to consume typed connection state.
- Migrate dashboard auth handoff to session snapshot/restore methods.
- Remove token/auth fields from
App where no longer required by views.
Phase 3: route sessions
- Add
WorkbenchSession and DashboardSession.
- Move cancellation and route-specific state into the owning session.
- Add lifecycle teardown.
- Keep current visual render modules initially; adapt them to narrower session/view interfaces.
Phase 4: supporting services
Extract in this order unless dependency analysis justifies another order:
ExportService
SchemaCatalogService
WorkbenchParameterSession
QueryDocumentSession and SavedQueryService
SchemaGraphSession
AppPreferences
Phase 5: composition cleanup
- Reduce
App/AppState to UI-only compatibility shapes, then remove them where possible.
- Split
renderApp into route shell mounting and focused view modules.
- Ensure
app.ts is composition/bootstrap only.
- Update architecture documentation.
Compatibility strategy
Temporary adapters are allowed during migration, for example:
app.actions.run = () => workbenchSession.runActiveSelection();
But they must be deleted before closing this issue. Do not preserve the original giant App interface as a permanent facade over all new services.
Avoid parallel duplicate implementations. At each phase, route every caller through the new service before deleting the previous implementation.
Testing requirements
Unit tests
Each extracted service/session must have focused tests constructed without a real browser DOM unless it is explicitly a UI adapter.
Use injected dependencies for:
- clock and wall clock
- random/query ID generation
- timers/sleep
- fetch/ClickHouse client
- persistence
- filesystem/export sink
Test cancellation and stale-result behavior explicitly.
Integration tests
Keep or add integration coverage proving:
- Workbench run updates the active document correctly
- Dashboard tiles use the same read execution semantics
- Workbench cancel does not cancel dashboard/export work
- Dashboard destruction cancels tile/filter work
- export cancellation does not cancel a grid run
- connection refresh is not raced independently by dashboard tiles
- session teardown removes listeners/timers/effects
Architecture tests
Add lightweight dependency guards, using ESLint/import rules or a small repository script, enforcing:
src/application/** cannot import src/ui/**
- route sessions cannot import each other's implementation modules
QueryExecutionService cannot import editor or rendering modules
DashboardSession cannot import Workbench modules
- non-view services cannot import DOM helpers
Do not add a large architecture framework solely for these checks.
Acceptance criteria
Definition of done metrics
These are review signals, not strict line-count goals:
app.ts should no longer be the largest implementation file because it contains application workflows.
- The public application/bootstrap object should expose route mounting and lifecycle, not hundreds of mutable fields.
- A unit test should be able to execute a query, run a dashboard tile, validate parameters, and export to an in-memory sink without constructing DOM nodes.
- Adding a future MCP-backed AI command should be possible by depending on typed application services/commands rather than importing
app.ts or mutating UI state.
Non-goals
- UI redesign
- changing the current saved-query JSON format
- implementing
DashboardDocument v1
- adding ClickHouse dashboard persistence
- implementing MCP or an AI helper
- introducing code splitting or lazy loading
- replacing signals
- introducing a UI framework
- converting the application to micro-frontends
- changing query semantics or export formats
Agent execution notes
Before editing code:
- Read
docs/ARCHITECTURE.md, src/ui/app.ts, src/ui/app.types.ts, src/state.ts, src/ui/dashboard.ts, src/ui/results.ts, and the ClickHouse client modules.
- Search all references to the specific functions being moved.
- Add characterization tests before moving behavior that is not already isolated.
- Move one responsibility at a time and run typecheck/tests after each move.
- Prefer preserving existing pure helpers and injecting them into services over copying logic.
- Do not weaken TypeScript types with broad
any, Record<string, unknown>, or a new universal context object to make extraction easier.
- Do not create a replacement god object named
Services, Context, or ApplicationState containing every dependency.
- Update import boundaries and delete obsolete compatibility members as each migration completes.
This issue may be implemented through a sequence of linked pull requests. Each PR should state which phase and acceptance criteria it completes.
Summary
Refactor
src/ui/app.tsfrom a large application controller/service locator into a small composition root plus explicit, constructible application services and route-scoped sessions.This is an architectural refactor. Preserve current behavior, build output, keyboard shortcuts, UI flows, persistence formats, authentication behavior, query semantics, cancellation semantics, and test coverage. Do not redesign the UI or introduce a framework as part of this issue.
The target is a modular monolith with clear boundaries for Workbench, Dashboard, future dashboard persistence, and an AI/MCP helper.
Why
src/ui/app.tscurrently owns or coordinates all of the following:The file is difficult to change safely because unrelated features share the same mutable
Appobject and globalAppState. Future product blocks—Workbench, Dashboard, persisted dashboard documents, share links, and an AI helper—need reusable application capabilities that do not depend on DOM nodes or the fullAppcontract.Architectural rules
Every extraction in this issue must follow these rules:
ApporAppStateas constructor arguments.renderResults,renderApp,flashToast, editor adapters, or UI modules.AppStateas a shortcut.Target module layout
Names may be adjusted slightly when implementation reveals a better fit, but preserve these responsibilities and boundaries.
Existing pure modules under
src/core, network code undersrc/net, editor ports, and focused UI render modules should remain where they are unless moving one is required to break an actual dependency cycle.Required extractions
1.
QueryExecutionServiceExtract the query transport and execution policy that currently exists through
runReadInto,run,runScript, query IDs, ClickHouse sessions, retry classification, parameter transport, and cancellation.The service must be constructible without the UI and usable by:
Suggested API shape:
Required behavior to preserve:
FORMAT/raw response handlingKILL QUERYTEMPORARYandSETSESSION_IS_LOCKED, or transient read-only statements where currently allowedDo not make this service write into tabs, history, dashboard tiles, DOM, or global signals.
2.
ConnectionSessionExtract authentication and ClickHouse connection lifecycle:
ensureAuthenticatedSuggested API:
The service must not render the login page or toast. Authentication failures must be represented as typed state or errors for the shell to handle.
3.
WorkbenchSessionCreate a route-scoped Workbench session that owns:
It may use signals internally. It must expose a narrow interface/view state to the Workbench shell.
It must provide
destroy()and release:Do not move Dashboard state into this session.
4.
DashboardSessionCreate a separate route-scoped Dashboard session that owns:
The Dashboard session must depend on
QueryExecutionServiceand dashboard/query repositories through narrow interfaces. It must not depend onWorkbenchSession, editor ports, Workbench DOM, or the completeAppobject.It must provide
destroy()and cancel all in-flight tile/filter work.This issue does not require persistent
DashboardDocument v1, but the session API must not assume that a dashboard is permanently derived from the favorites list. Accept a dashboard runtime input/model through an explicit interface so a stored document can replace the current source later.5.
ExportServiceExtract:
Separate application policy from browser filesystem access:
ExportServicemust be testable using an in-memory sink. It may depend on query/connection primitives, but must not use Workbench DOM or mutateAppStatedirectly.6.
SchemaCatalogServiceExtract server metadata and reference-data lifecycle:
Suggested API:
Keep rendering of the schema tree and connection banner in UI modules.
7.
WorkbenchParameterSessionExtract Workbench parameter state and policy currently mixed into
renderVarStripand query execution:Suggested API:
The session returns view models. A focused
VarStripViewremains responsible for DOM controls, focus preservation, combobox behavior, and painting diagnostics.8.
QueryDocumentSessionandSavedQueryServiceSeparate an open document from its persisted representation.
QueryDocumentSessionowns:SavedQueryServiceowns:Do not let saved-query persistence call editor or rendering methods. The Workbench session coordinates persistence results with its document state.
9.
SchemaGraphSessionExtract schema-lineage operation state:
Suggested API:
The service/session returns graph models. Opening a child window/overlay and drawing SVG remain UI adapter responsibilities.
10.
AppPreferencesCentralize typed access to persisted user preferences and local/session storage keys.
At minimum include:
Do not force saved-query/library domain records into a generic key-value API if a dedicated repository is clearer.
app.tstarget responsibilityAt completion,
src/ui/app.tsshould be a composition/bootstrap module. It may:ApplicationlifecycleIt should not contain business workflows such as query execution, export streaming, schema graph loading, saved-query commits, parameter validation, or authentication refresh logic.
Illustrative target:
The final implementation does not need to match this code literally, but it must produce the same dependency direction.
Migration plan
Implement as incremental PRs or commits. Do not rewrite
app.tsin one pass.Phase 0: characterization and dependency map
app.tsline count andAppinterface member count in the PR description, not as a permanent test.App,AppState,runReadInto, auth helpers, and route-specific state.Required characterization coverage:
FORMATPhase 1: QueryExecutionService
apptemporarily where needed.app.runReadIntoafter all callers migrate.Phase 2: ConnectionSession
Appwhere no longer required by views.Phase 3: route sessions
WorkbenchSessionandDashboardSession.Phase 4: supporting services
Extract in this order unless dependency analysis justifies another order:
ExportServiceSchemaCatalogServiceWorkbenchParameterSessionQueryDocumentSessionandSavedQueryServiceSchemaGraphSessionAppPreferencesPhase 5: composition cleanup
App/AppStateto UI-only compatibility shapes, then remove them where possible.renderAppinto route shell mounting and focused view modules.app.tsis composition/bootstrap only.Compatibility strategy
Temporary adapters are allowed during migration, for example:
But they must be deleted before closing this issue. Do not preserve the original giant
Appinterface as a permanent facade over all new services.Avoid parallel duplicate implementations. At each phase, route every caller through the new service before deleting the previous implementation.
Testing requirements
Unit tests
Each extracted service/session must have focused tests constructed without a real browser DOM unless it is explicitly a UI adapter.
Use injected dependencies for:
Test cancellation and stale-result behavior explicitly.
Integration tests
Keep or add integration coverage proving:
Architecture tests
Add lightweight dependency guards, using ESLint/import rules or a small repository script, enforcing:
src/application/**cannot importsrc/ui/**QueryExecutionServicecannot import editor or rendering modulesDashboardSessioncannot import Workbench modulesDo not add a large architecture framework solely for these checks.
Acceptance criteria
QueryExecutionServiceis constructible and tested withoutApp,AppState, or DOM.ConnectionSessionwithout rendering dependencies.destroy()and tests prove cleanup.ExportServicewith an injectable filesystem sink.SchemaCatalogService.QueryDocumentSession.app.ts.Appobject.src/application/**has no imports fromsrc/ui/**.dist/sql.html.src/ui/app.tsbecomes composition/bootstrap code rather than a business-workflow controller.docs/ARCHITECTURE.mdis updated with the new dependency graph and lifecycle ownership.Definition of done metrics
These are review signals, not strict line-count goals:
app.tsshould no longer be the largest implementation file because it contains application workflows.app.tsor mutating UI state.Non-goals
DashboardDocument v1Agent execution notes
Before editing code:
docs/ARCHITECTURE.md,src/ui/app.ts,src/ui/app.types.ts,src/state.ts,src/ui/dashboard.ts,src/ui/results.ts, and the ClickHouse client modules.any,Record<string, unknown>, or a new universal context object to make extraction easier.Services,Context, orApplicationStatecontaining every dependency.This issue may be implemented through a sequence of linked pull requests. Each PR should state which phase and acceptance criteria it completes.