From 5eace109276f464cc591f641c6a3e0a145d58fdf Mon Sep 17 00:00:00 2001 From: Luke Cotter <4013877+lukecotter@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:56:55 +0100 Subject: [PATCH 1/3] fix(log-viewer): keep the shared hairline visible on themes with no widget.border `--lana-surface-border` ended its fallback chain in `transparent`, so on a theme that sets neither `widget.border` nor `sideBar.border` the token was defined but invisible, and the consumer fallbacks behind it could no longer fire. The minimap frame, the timeline label leader lines, the gauge and sparkline tracks, the grid header rule and the find widget outline all lost their line. End the chain in `panel-border`, which every theme registers, and in a literal for a host outside VS Code. --- log-viewer/src/styles/tokens.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/log-viewer/src/styles/tokens.css b/log-viewer/src/styles/tokens.css index 9626bc27..75009327 100644 --- a/log-viewer/src/styles/tokens.css +++ b/log-viewer/src/styles/tokens.css @@ -63,11 +63,11 @@ --lana-font-ui: var(--vscode-font-family, sans-serif); /* Surfaces — the same registered colors VS Code's own chrome is built from. - `surface-border` is absent before the size-token registry (stable 1.128 ships - none of the `--vscode-surface-*` set), so its fallback chain is load-bearing. */ + `surface-border` ships in no stable VS Code and most themes set no `widget.border`, + so the chain has to reach a colour or every hairline drawn with it disappears. */ --lana-surface-border: var( --vscode-surface-border, - var(--vscode-widget-border, var(--vscode-sideBar-border, transparent)) + var(--vscode-widget-border, var(--vscode-panel-border, rgba(128, 128, 128, 0.35))) ); --lana-header-bg: var(--vscode-tab-activeBackground, var(--lana-editor-bg)); From 2bd0f97711674ca218c85b33fe10f213cbfbbea5 Mon Sep 17 00:00:00 2001 From: Luke Cotter <4013877+lukecotter@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:57:06 +0100 Subject: [PATCH 2/3] docs: cut the agent rules to the rule `.claude/rules/log-viewer.md` had grown rationale prose around each rule, so the rule itself was hard to find. Rewrite it to 48 lines from 73, keeping every rule, and add the one the token fix earns: a fallback chain must not end in `transparent`. Record the comment rule in `AGENTS.md`: only what the code cannot say, one short line, only where needed. --- .claude/rules/log-viewer.md | 77 +++++++++++++------------------------ AGENTS.md | 1 + 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/.claude/rules/log-viewer.md b/.claude/rules/log-viewer.md index ef53aead..18776808 100644 --- a/.claude/rules/log-viewer.md +++ b/.claude/rules/log-viewer.md @@ -5,68 +5,45 @@ paths: # log-viewer rules -Webview UI. Applies when working under `log-viewer/`. +Webview UI. ## Boundary -- MUST NOT import `vscode` or anything from `lana/`. -- Communicate with the extension via message passing only. +- Never import `vscode` or anything from `lana/`. Message passing only. -## Performance budgets +## Performance -- Parse + render: `<5MB` → `<1s`, `10MB` → `<3s`, `20MB+` → `<5s`. -- No synchronous operations >50ms blocking the extension host. -- Operations >100ms show a progress indicator. -- Benchmark against large logs from `sample-app/`. +- Parse + render: `<5MB` in `<1s`, `10MB` in `<3s`, `20MB+` in `<5s`. Benchmark on `sample-app/` logs. +- Nothing synchronous over 50ms. Show progress over 100ms. -## Theme changes +## Theme -- The panel keeps its context and is never re-created, so a theme switch has to be observed at - runtime. HTML re-themes itself through `--vscode-*`; anything drawn on a canvas does not. - Canvas code reads its colours through `themeObserver.on(…)` and repaints, and must never - re-initialise the renderer to do it. +- The panel is never re-created, so a theme switch is observed at runtime. HTML re-themes through + `--vscode-*`; canvas does not — read colours via `themeObserver.on(…)` and repaint, never + re-initialise the renderer. +- Check every change in a light and a dark theme. -## UI appearance +## Appearance -- **Never write a raw literal for a color, radius, space, shadow or border width.** Use a - `--lana-*` token, or the `--vscode-*` var the value comes from. Replace the literals in the code - you touch; older files still hold them and we convert file by file. -- **A `--lana-*` token when the value is more than one var, and the plain `--vscode-*` var when it - is not.** Give a value a token when any of these holds: - 1. the role appears in three or more files; - 2. VS Code has no var for it, so it is a scale or a role of ours (`--lana-space-*`, - `--lana-pane-min`); - 3. the value is a chain, a `calc` or a `color-mix`, not a single var; - 4. the var is absent in a VS Code we support, so a fallback is needed — `cornerRadius-*`, - `spacing-size*` and `strokeThickness` ship in no stable release. - - Otherwise use the var directly. A token wrapping one already-semantic var used in one file - (`--vscode-editorCursor-foreground`) only adds a name to learn and a hop to trace. - -- **Put a new token in `styles/tokens.css` as `var(--vscode-…, )`.** The literal is what a - host outside VS Code falls back to, and what covers a var VS Code has yet to register. -- **Never define or override a `--vscode-*` name.** An override is global to the webview, so - re-skinning one role changes every other consumer of that var, and shadowing a platform name - leaves no way to tell what the var means. A host outside VS Code supplies the whole `--vscode-*` - block once instead. The one exception is skinning a `vscode-elements` component, which reads its - own `--vscode-*` names: scope the override to that element, never to `:host` or `:root`. -- **Data palettes stay literal.** The timeline categories (`timeline/themes/Themes.ts`) and the - metric-strip tiers (`metric-strip/metric-strip-colors.ts`) show meaning, not chrome, so they do - not follow the host theme. -- **A component that names a token must also carry the tokens.** `globalStyles` carries them, so - `static styles = [globalStyles, …]` is enough. Without `globalStyles`, add `tokenStyles` - (`styles/tokens.styles.ts`). The document copy, which the build injects, styles only the popups - that tabulator puts in `document.body`. -- **Check each UI change in a light theme and in a dark theme.** +- Write no literal colour, radius, space, shadow or border width. Use a `--lana-*` token, or the + `--vscode-*` var the value comes from, and convert the literals in the code you touch. +- Make it a token when the role is in three or more files, is ours (`--lana-space-*`, + `--lana-pane-min`), is a chain, `calc` or `color-mix`, or the var ships in no stable VS Code + (`cornerRadius-*`, `spacing-size*`, `strokeThickness`). Otherwise use the var directly. +- New tokens go in `styles/tokens.css` as `var(--vscode-…, )`; the literal is what a host + outside VS Code gets. Never end a chain in `transparent` — the value is then defined but invisible, + and no consumer fallback can fire. +- Never define or override a `--vscode-*` name — an override is global to the webview. Exception: + skinning a `vscode-elements` component; scope it to that element, never `:host` or `:root`. +- Data palettes stay literal: they show meaning, not chrome (`timeline/themes/Themes.ts`, + `metric-strip/metric-strip-colors.ts`). +- A component naming a token must carry the tokens: `globalStyles`, or `tokenStyles` + (`styles/tokens.styles.ts`) without it. The document copy styles only tabulator's body popups. ## Key paths -- Timeline: `log-viewer/src/features/timeline/` -- Parser: `log-viewer/src/core/log-parser/` -- Theme observer: `log-viewer/src/core/theme/ThemeObserver.ts` -- Tokens: `log-viewer/src/styles/tokens.css` +`features/timeline/` · `core/log-parser/` · `core/theme/ThemeObserver.ts` · `styles/tokens.css` ## Testing -- Features and bug fixes include tests. -- Breaking changes to log parsing cover both old and new formats. +- Features and fixes ship with tests. Parser format changes cover the old and the new format. diff --git a/AGENTS.md b/AGENTS.md index 7563fe14..a6bf633f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,7 @@ the TS ≤6.0 API (lands in TS 7.1). Don't remove until typescript-eslint suppor - **Performance** — handle large logs (50MB+, 500k+ lines) without blocking the UI. - **UX** — discoverable, accessible, actionable errors. - **Testing** — features and bug fixes ship with tests; CI blocks failures. +- **Comments** — only what the code cannot say, one short line, and only where needed. ## Critical boundary From 5b799fbb78eaafb941926a8c01915679abdb0776 Mon Sep 17 00:00:00 2001 From: Luke Cotter <4013877+lukecotter@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:57:06 +0100 Subject: [PATCH 3/3] refactor(log-viewer): deliver the log through context only The log reached the views by two paths: the `logContext` provider and a `log:loaded` event that five components listened to through `LogLoadedController`. A new log is a new store, so the context alone is enough to rebuild every consumer. Retire the controller and the event, and let the five components consume the store. Two `currentLogStore()` wrappers are orphaned by the move and go with it. --- log-viewer/src/components/CallTreeDetail.ts | 24 ++++++------ log-viewer/src/components/HotPath.ts | 18 ++++++--- log-viewer/src/components/HotSpots.ts | 18 ++++++--- .../src/components/__tests__/HotPath.test.ts | 4 +- .../src/components/__tests__/HotSpots.test.ts | 4 +- log-viewer/src/core/events/EventBus.ts | 6 --- .../src/core/events/LogLoadedController.ts | 39 ------------------- .../analysis/components/LogDiagnosticsView.ts | 24 ++++++------ .../__tests__/LogDiagnosticsView.test.ts | 10 +++-- log-viewer/src/features/app/LogViewer.ts | 3 -- .../call-tree/utils/ExecutionHighlights.ts | 7 ---- .../database/components/DatabaseTimeTree.ts | 30 ++++++++------ .../database/services/databaseOverview.ts | 7 ---- 13 files changed, 81 insertions(+), 113 deletions(-) delete mode 100644 log-viewer/src/core/events/LogLoadedController.ts diff --git a/log-viewer/src/components/CallTreeDetail.ts b/log-viewer/src/components/CallTreeDetail.ts index e2f05a9e..00fafb94 100644 --- a/log-viewer/src/components/CallTreeDetail.ts +++ b/log-viewer/src/components/CallTreeDetail.ts @@ -1,6 +1,7 @@ /* * Copyright (c) 2026 Certinia Inc. All rights reserved. */ +import { consume } from '@lit/context'; import type { LogEvent } from 'apex-log-parser'; import { LitElement, css, html, unsafeCSS, type PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; @@ -12,7 +13,8 @@ import { } from 'tabulator-tables'; import { eventBus } from '../core/events/EventBus.js'; -import { LogLoadedController } from '../core/events/LogLoadedController.js'; +import { logContext } from '../core/log/logContext.js'; +import type { LogStore } from '../core/log/LogStore.js'; import { formatDuration, formatInteger } from '../core/utility/Util.js'; import { commonColumnDefaults, @@ -186,16 +188,10 @@ export class CallTreeDetail extends LitElement { 'bottom-up': null, }; - // A whole-log tree can mount before the first parse finishes (the scoped - // tree cannot — a selection implies a parsed log), so rebuild when the log - // lands. - private readonly _logLoaded = new LogLoadedController(this, () => { - if (!this.wholeLog) { - return; - } - this._invalidateScope(); - void this._showActive(); - }); + /** The log on screen, from the app root. */ + @consume({ context: logContext, subscribe: true }) + @property({ attribute: false }) + logStore: LogStore | null = null; constructor() { super(); @@ -316,7 +312,11 @@ export class CallTreeDetail extends LitElement { ]; updated(changed: PropertyValues) { - const scopeChanged = changed.has('eventIndex') || changed.has('instances'); + // Only the whole-log tree can mount before a parse, so there a new log is a new scope. + const scopeChanged = + (changed.has('logStore') && this.wholeLog) || + changed.has('eventIndex') || + changed.has('instances'); if (scopeChanged) { this._invalidateScope(); } diff --git a/log-viewer/src/components/HotPath.ts b/log-viewer/src/components/HotPath.ts index e29177ae..48d2570a 100644 --- a/log-viewer/src/components/HotPath.ts +++ b/log-viewer/src/components/HotPath.ts @@ -1,15 +1,17 @@ /* * Copyright (c) 2026 Certinia Inc. All rights reserved. */ +import { consume } from '@lit/context'; import { LitElement, css, html } from 'lit'; -import { customElement } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import '#vscode-elements/vscode-icon.js'; -import { LogLoadedController } from '../core/events/LogLoadedController.js'; +import { logContext } from '../core/log/logContext.js'; +import type { LogStore } from '../core/log/LogStore.js'; import { formatDuration } from '../core/utility/Util.js'; import { - getCurrentExecutionHighlights, + getExecutionHighlights, type ExecutionHighlights, type HotPathFrame, } from '../features/call-tree/utils/ExecutionHighlights.js'; @@ -31,8 +33,11 @@ const FRAME_CAP = 10; */ @customElement('hot-path') export class HotPath extends LitElement { - /** The path has to follow the log itself. */ - private readonly _logLoaded = new LogLoadedController(this); + /** The log on screen, from the app root. */ + @consume({ context: logContext, subscribe: true }) + @property({ attribute: false }) + logStore: LogStore | null = null; + private readonly _palette = new CategoryPaletteController(this); static styles = [ @@ -69,7 +74,8 @@ export class HotPath extends LitElement { ]; render() { - const highlights = getCurrentExecutionHighlights(); + const log = this.logStore?.log; + const highlights = log && getExecutionHighlights(log); if (!highlights || !highlights.hotPath.length) { return html`

The log has no timed calls.

`; } diff --git a/log-viewer/src/components/HotSpots.ts b/log-viewer/src/components/HotSpots.ts index c337b049..673a3131 100644 --- a/log-viewer/src/components/HotSpots.ts +++ b/log-viewer/src/components/HotSpots.ts @@ -1,14 +1,16 @@ /* * Copyright (c) 2026 Certinia Inc. All rights reserved. */ +import { consume } from '@lit/context'; import { LitElement, html } from 'lit'; -import { customElement } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; -import { LogLoadedController } from '../core/events/LogLoadedController.js'; +import { logContext } from '../core/log/logContext.js'; +import type { LogStore } from '../core/log/LogStore.js'; import { formatDuration } from '../core/utility/Util.js'; import { - getCurrentExecutionHighlights, + getExecutionHighlights, type HotSpotRow, } from '../features/call-tree/utils/ExecutionHighlights.js'; import { globalStyles } from '../styles/global.styles.js'; @@ -25,14 +27,18 @@ import { dispatchInspectorReveal } from './inspectorReveal.js'; */ @customElement('hot-spots') export class HotSpots extends LitElement { - /** The list has to follow the log itself. */ - private readonly _logLoaded = new LogLoadedController(this); + /** The log on screen, from the app root. */ + @consume({ context: logContext, subscribe: true }) + @property({ attribute: false }) + logStore: LogStore | null = null; + private readonly _palette = new CategoryPaletteController(this); static styles = [globalStyles, inspectorSectionStyles, revealRowStyles]; render() { - const highlights = getCurrentExecutionHighlights(); + const log = this.logStore?.log; + const highlights = log && getExecutionHighlights(log); if (!highlights || !highlights.hotSpots.length) { return html`

The log has no timed calls.

`; } diff --git a/log-viewer/src/components/__tests__/HotPath.test.ts b/log-viewer/src/components/__tests__/HotPath.test.ts index 522544f5..b44b059f 100644 --- a/log-viewer/src/components/__tests__/HotPath.test.ts +++ b/log-viewer/src/components/__tests__/HotPath.test.ts @@ -5,13 +5,14 @@ */ import { beforeEach, describe, expect, it } from '@jest/globals'; +import type { LogStore } from '../../core/log/LogStore.js'; import type { ExecutionHighlights } from '../../features/call-tree/utils/ExecutionHighlights.js'; jest.mock('#vscode-elements/vscode-icon.js', () => ({})); let highlights: ExecutionHighlights | null = null; jest.mock('../../features/call-tree/utils/ExecutionHighlights.js', () => ({ - getCurrentExecutionHighlights: () => highlights, + getExecutionHighlights: () => highlights, })); import '../HotPath.js'; @@ -35,6 +36,7 @@ const pathOf = (frameCount: number): ExecutionHighlights => ({ const hotPath = async () => { const element = document.createElement('hot-path'); + element.logStore = { log: {} } as unknown as LogStore; document.body.append(element); await element.updateComplete; return element; diff --git a/log-viewer/src/components/__tests__/HotSpots.test.ts b/log-viewer/src/components/__tests__/HotSpots.test.ts index e59c823b..14915021 100644 --- a/log-viewer/src/components/__tests__/HotSpots.test.ts +++ b/log-viewer/src/components/__tests__/HotSpots.test.ts @@ -5,11 +5,12 @@ */ import { beforeEach, describe, expect, it } from '@jest/globals'; +import type { LogStore } from '../../core/log/LogStore.js'; import type { ExecutionHighlights } from '../../features/call-tree/utils/ExecutionHighlights.js'; let highlights: ExecutionHighlights | null = null; jest.mock('../../features/call-tree/utils/ExecutionHighlights.js', () => ({ - getCurrentExecutionHighlights: () => highlights, + getExecutionHighlights: () => highlights, })); import '../HotSpots.js'; @@ -32,6 +33,7 @@ const spotsOf = (): ExecutionHighlights => ({ const hotSpots = async () => { const element = document.createElement('hot-spots'); + element.logStore = { log: {} } as unknown as LogStore; document.body.append(element); await element.updateComplete; return element; diff --git a/log-viewer/src/core/events/EventBus.ts b/log-viewer/src/core/events/EventBus.ts index 1c287323..a644041a 100644 --- a/log-viewer/src/core/events/EventBus.ts +++ b/log-viewer/src/core/events/EventBus.ts @@ -32,12 +32,6 @@ export type DetailSelection = | { kind: 'aggregate'; instances: number[]; label: string }; interface EventMap { - // A log finished parsing and the event-lookup service holds it. Whole-log - // content reads that service directly, so it needs telling once the data is - // there; the first paint happens before any log exists. No payload: a listener - // that needs the log asks the service for it, keeping one source of truth. - 'log:loaded': Record; - // Supply eventIndex (preferred — unique) OR timestamp (fallback for raw-log entry where eventIndex isn't known). 'timeline:navigate-to': diff --git a/log-viewer/src/core/events/LogLoadedController.ts b/log-viewer/src/core/events/LogLoadedController.ts deleted file mode 100644 index 23b473f6..00000000 --- a/log-viewer/src/core/events/LogLoadedController.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2026 Certinia Inc. All rights reserved. - */ -import type { ReactiveController, ReactiveControllerHost } from 'lit'; - -import { eventBus } from './EventBus.js'; - -/** - * Re-runs `onLoad` (default: a host re-render) whenever a log finishes parsing. - * The inspector's sections paint before the first parse and rebuild only on a - * tab change or a selection, so a section that reads the log has to follow the - * log itself. - */ -export class LogLoadedController implements ReactiveController { - private readonly _host: ReactiveControllerHost; - private readonly _onLoad?: () => void; - private _off: (() => void) | null = null; - - constructor(host: ReactiveControllerHost, onLoad?: () => void) { - this._host = host; - this._onLoad = onLoad; - host.addController(this); - } - - hostConnected(): void { - this._off = eventBus.on('log:loaded', () => { - if (this._onLoad) { - this._onLoad(); - } else { - this._host.requestUpdate(); - } - }); - } - - hostDisconnected(): void { - this._off?.(); - this._off = null; - } -} diff --git a/log-viewer/src/features/analysis/components/LogDiagnosticsView.ts b/log-viewer/src/features/analysis/components/LogDiagnosticsView.ts index c2c27887..9396d682 100644 --- a/log-viewer/src/features/analysis/components/LogDiagnosticsView.ts +++ b/log-viewer/src/features/analysis/components/LogDiagnosticsView.ts @@ -2,11 +2,13 @@ * Copyright (c) 2026 Certinia Inc. All rights reserved. */ import '#vscode-elements/vscode-icon.js'; -import { LitElement, css, html, unsafeCSS } from 'lit'; +import { consume } from '@lit/context'; +import { LitElement, css, html, unsafeCSS, type PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { dispatchInspectorReveal } from '../../../components/inspectorReveal.js'; -import { eventBus } from '../../../core/events/EventBus.js'; +import { logContext } from '../../../core/log/logContext.js'; +import type { LogStore } from '../../../core/log/LogStore.js'; import { formatDuration } from '../../../core/utility/Util.js'; import { formatSOQLToTemplate } from '../../soql/format/formatter.js'; import { SEVERITY_TYPES, type Severity } from '../../soql/services/SOQLLinter.js'; @@ -67,6 +69,11 @@ export class LogDiagnosticsView extends LitElement { @property({ attribute: false }) instances: readonly number[] | null = null; + /** The log on screen, from the app root. */ + @consume({ context: logContext, subscribe: true }) + @property({ attribute: false }) + logStore: LogStore | null = null; + /** The whole log's findings, before any scoping. */ @state() private _all: LogDiagnostics | null = null; @@ -92,18 +99,12 @@ export class LogDiagnosticsView extends LitElement { /** The findings {@link _result} was scoped from. */ private _scoped: LogDiagnostics | null = null; - private _offLogLoaded: (() => void) | null = null; - private _columns = FALLBACK_COLUMNS; private _resize: ResizeObserver | null = null; override connectedCallback() { super.connectedCallback(); - void this._analyse(); - // The inspector paints before the first log is parsed, and it rebuilds only - // on a tab change or a selection. - this._offLogLoaded = eventBus.on('log:loaded', () => void this._analyse()); if (typeof ResizeObserver !== 'undefined') { this._resize = new ResizeObserver(() => this._measure()); this._resize.observe(this); @@ -111,8 +112,6 @@ export class LogDiagnosticsView extends LitElement { } override disconnectedCallback() { - this._offLogLoaded?.(); - this._offLogLoaded = null; this._resize?.disconnect(); this._resize = null; super.disconnectedCallback(); @@ -135,7 +134,10 @@ export class LogDiagnosticsView extends LitElement { this._all && this.instances ? scopeDiagnostics(this._all, this.instances) : this._all; } - override updated() { + override updated(changed: PropertyValues) { + if (changed.has('logStore')) { + void this._analyse(); + } this._measure(); } diff --git a/log-viewer/src/features/analysis/components/__tests__/LogDiagnosticsView.test.ts b/log-viewer/src/features/analysis/components/__tests__/LogDiagnosticsView.test.ts index 586e7729..78ec13ff 100644 --- a/log-viewer/src/features/analysis/components/__tests__/LogDiagnosticsView.test.ts +++ b/log-viewer/src/features/analysis/components/__tests__/LogDiagnosticsView.test.ts @@ -28,9 +28,13 @@ jest.mock('../../services/LogDiagnostics.js', () => ({ }, })); -import { eventBus } from '../../../../core/events/EventBus.js'; +import type { LogStore } from '../../../../core/log/LogStore.js'; import '../LogDiagnosticsView.js'; +const loadLog = (element: HTMLElementTagNameMap['log-diagnostics']) => { + element.logStore = { log: {} } as unknown as LogStore; +}; + const view = async (scope?: { instances: number[] }) => { const element = document.createElement('log-diagnostics'); if (scope) { @@ -286,7 +290,7 @@ describe('log-diagnostics', () => { eventIndex: 1, }, ]; - eventBus.emit('log:loaded', {}); + loadLog(element); await element.updateComplete; await element.updateComplete; expect(text(element, '.title')).toEqual(['Later finding.']); @@ -369,7 +373,7 @@ describe('log-diagnostics', () => { // The next log has nothing at that severity, and with one band left there is // no roll-up to release it with, so the filter must not outlive the list. result = { ...result, diagnostics: [result.diagnostics[1]!] }; - eventBus.emit('log:loaded', {}); + loadLog(element); await element.updateComplete; await element.updateComplete; expect(text(element, '.title')).toEqual(['Noted.']); diff --git a/log-viewer/src/features/app/LogViewer.ts b/log-viewer/src/features/app/LogViewer.ts index 3e04d7db..465e26d9 100644 --- a/log-viewer/src/features/app/LogViewer.ts +++ b/log-viewer/src/features/app/LogViewer.ts @@ -267,9 +267,6 @@ export class LogViewer extends LitElement { // Published before the views render, so every tab reads the same log // whichever one loads first. this._logStore = setCurrentLog(apexLog); - // After the store holds the log, never before: the views still on the event - // read it straight from there. - eventBus.emit('log:loaded', {}); this.logSize = apexLog.size; this.timelineRoot = apexLog; diff --git a/log-viewer/src/features/call-tree/utils/ExecutionHighlights.ts b/log-viewer/src/features/call-tree/utils/ExecutionHighlights.ts index bc88d106..ba7f7fca 100644 --- a/log-viewer/src/features/call-tree/utils/ExecutionHighlights.ts +++ b/log-viewer/src/features/call-tree/utils/ExecutionHighlights.ts @@ -3,7 +3,6 @@ */ import type { ApexLog, LogCategory, LogEvent } from 'apex-log-parser'; -import { currentLogStore } from '../../../core/log/LogStore.js'; import { getEventKey } from './Aggregation.js'; /** One frame on the hot path, entry point first. */ @@ -245,9 +244,3 @@ export function getExecutionHighlights(apexLog: ApexLog): ExecutionHighlights { } return highlights; } - -/** The highlights for the log on screen, or null before the first parse. */ -export function getCurrentExecutionHighlights(): ExecutionHighlights | null { - const apexLog = currentLogStore()?.log; - return apexLog ? getExecutionHighlights(apexLog) : null; -} diff --git a/log-viewer/src/features/database/components/DatabaseTimeTree.ts b/log-viewer/src/features/database/components/DatabaseTimeTree.ts index 1e5a16e6..c9cdface 100644 --- a/log-viewer/src/features/database/components/DatabaseTimeTree.ts +++ b/log-viewer/src/features/database/components/DatabaseTimeTree.ts @@ -1,8 +1,9 @@ /* * Copyright (c) 2026 Certinia Inc. All rights reserved. */ -import { LitElement, css, html, unsafeCSS } from 'lit'; -import { customElement } from 'lit/decorators.js'; +import { consume } from '@lit/context'; +import { LitElement, css, html, unsafeCSS, type PropertyValues } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; import { type CellComponent, type ColumnDefinition, @@ -23,7 +24,8 @@ import { } from '../../../components/locatedRow.js'; import { PANEL_ROW_MENU_ITEMS, runPanelRowAction } from '../../../components/panelRowMenu.js'; import { eventBus } from '../../../core/events/EventBus.js'; -import { LogLoadedController } from '../../../core/events/LogLoadedController.js'; +import { logContext } from '../../../core/log/logContext.js'; +import type { LogStore } from '../../../core/log/LogStore.js'; import { SelectionEchoGuard } from '../../../core/events/SelectionEchoGuard.js'; import { formatDuration, formatInteger } from '../../../core/utility/Util.js'; import { globalStyles } from '../../../styles/global.styles.js'; @@ -43,7 +45,7 @@ import { soqlInlineElement } from '../../soql/format/inlineCell.js'; import { soqlSyntaxStyles } from '../../soql/styles/soql-syntax.css.js'; import { type DatabaseCallNode, - currentDatabaseOverview, + databaseOverview, NO_STATEMENTS, type StatementKind, } from '../services/databaseOverview.js'; @@ -158,9 +160,10 @@ export class DatabaseTime extends LitElement { /** The build in flight; a newer one aborts it, and so does a disconnect. */ private _building: AbortController | null = null; - private readonly _logLoaded = new LogLoadedController(this, () => { - void this._build(); - }); + /** The log on screen, from the app root. */ + @consume({ context: logContext, subscribe: true }) + @property({ attribute: false }) + logStore: LogStore | null = null; static styles = [ globalStyles, @@ -222,7 +225,12 @@ export class DatabaseTime extends LitElement { firstUpdated(): void { this._contextMenu = this.renderRoot.querySelector('context-menu'); - void this._build(); + } + + updated(changed: PropertyValues): void { + if (changed.has('logStore')) { + void this._build(); + } } render() { @@ -271,7 +279,8 @@ export class DatabaseTime extends LitElement { } private async _build(): Promise { - const overview = currentDatabaseOverview(); + const log = this.logStore?.log; + const overview = log && databaseOverview(log); this._building?.abort(); const { signal } = (this._building = new AbortController()); // Wait for the host to lay out before Tabulator measures column widths — @@ -295,8 +304,7 @@ export class DatabaseTime extends LitElement { return; } if (!overview) { - // No log yet, so there is nothing to size a new table against; the load - // event builds it once the figures exist. + // No log yet, so there is nothing to size a new table against. return; } diff --git a/log-viewer/src/features/database/services/databaseOverview.ts b/log-viewer/src/features/database/services/databaseOverview.ts index ce2dcb8d..a763e350 100644 --- a/log-viewer/src/features/database/services/databaseOverview.ts +++ b/log-viewer/src/features/database/services/databaseOverview.ts @@ -10,7 +10,6 @@ import { } from 'apex-log-parser'; import { DEFAULT_NAMESPACE, getCallerNamespace } from '../../../core/utility/CallerNamespace.js'; -import { currentLogStore } from '../../../core/log/LogStore.js'; /** The label for a DML statement whose SObject the log never names. */ export const UNKNOWN_OBJECT = 'Unknown'; @@ -164,12 +163,6 @@ export interface DatabaseOverview { /** Memo per log: the tree never changes after parse, the sections re-render. */ const cache = new WeakMap(); -/** The whole-log figures for the log on screen, or `null` before one resolves. */ -export function currentDatabaseOverview(): DatabaseOverview | null { - const apexLog = currentLogStore()?.log; - return apexLog ? databaseOverview(apexLog) : null; -} - /** {@link DatabaseOverview} for a parsed log, computed once. */ export function databaseOverview(root: ApexLog): DatabaseOverview { const cached = cache.get(root);