Console panel: capture console.*, timestamped lines, resizable (#50)

A log layer (src/editor/console/log.ts) keeps a 500-line ring buffer and
wraps console.log/info/debug/warn/error, still calling the original. Notices
and behaviour runtime errors go straight to the layer instead of through
console, so editor tests stay quiet and errors can render red. world.log is
untouched: it is per-run structured state that runtime tests and e2e read
directly, not a text stream.

The panel sits under the visible tab, resizable with Splitter('y'), with a
CONSOLE header and a clear button. Each line carries its wall-clock time in
its own dimmed column, so locate() still finds the message text.

The taller centre area drops the graph fit zoom to 0.60, which broke six e2e
checks that assumed one screen pixel is one graph unit and that a fixed
offset from the canvas centre is empty. They now ask __gm.emptySpot(),
__gm.graph.emptySpot() and __gm.graph.zoom() instead of guessing.

locate/locateAll added the unscaled Pixi text width to the global position,
so click points in the scaled graph canvas landed too far right; they use
getBounds() now.

check.sh: 831 tests. Full e2e: ALL PASSED - 671 checks. Smokes 24/19/20.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-12 19:35:40 +02:00
1 parent e7d82bd1a9
commit c47fd5e4dc
25 files changed
+690 -42

No files matched your search

+12
View File
@@ -81,6 +81,9 @@ export type TextStyleToken =
| 'live'
| 'title'
| 'mono'
| 'monoMuted'
| 'monoWarn'
| 'monoError'
| 'accent'
| 'viewportLabel'
| 'viewportLabelMuted';
@@ -124,6 +127,15 @@ export function textStyle(token: TextStyleToken = 'body'): TextStyle {
case 'mono':
style = new TextStyle({ ...base, fontFamily: Theme.monoFont });
break;
case 'monoMuted':
style = new TextStyle({ ...base, fontFamily: Theme.monoFont, fill: Theme.colors.textMuted });
break;
case 'monoWarn':
style = new TextStyle({ ...base, fontFamily: Theme.monoFont, fill: Theme.colors.override });
break;
case 'monoError':
style = new TextStyle({ ...base, fontFamily: Theme.monoFont, fill: Theme.colors.danger });
break;
case 'accent':
style = new TextStyle({ ...base, fill: Theme.colors.accentText, fontWeight: '600' });
break;
+3 -1
View File
@@ -14,6 +14,8 @@ export interface PanelOptions {
/** A titled box; the header row can hold extra widgets through `header.add()`. */
export class Panel extends Widget {
/** Height of the title row – the floor for a panel that must stay grabbable when dragged shut. */
static readonly HEADER_HEIGHT = Theme.rowHeight + 4;
private readonly bg = new Graphics();
readonly header: HStack;
readonly content: VStack;
@@ -25,7 +27,7 @@ export class Panel extends Widget {
this.chrome = options.chrome ?? true;
this.addChild(this.bg);
this.header = new HStack({ padding: 0, gap: Theme.gap, align: 'center' });
this.header.minHeight = Theme.rowHeight + 4;
this.header.minHeight = Panel.HEADER_HEIGHT;
if (options.title !== undefined) {
this.titleLabel = new Label(options.title.toUpperCase(), { style: 'title', padding: Theme.padding });
this.header.add(this.titleLabel, 1);
+5
View File
@@ -53,6 +53,11 @@ export class ScrollContainer extends Widget {
return this.content ? this.content.h : 0;
}
/** Current scroll offset; equal to `maxScroll` means the end of the content is showing. */
get offset(): number {
return this.scrollY;
}
/** Largest valid scroll offset; 0 while the content fits. */
get maxScroll(): number {
return Math.max(0, this.contentHeight - this.h);