mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(coding-agent): play a 3D Armin on /arminsayshi in fullscreen mode
Replaces the 3D pi logo easter egg on header logo click. Armin's bitmap is built from one cube per pixel, using the theme accent color. Regular mode keeps the inline Armin art.
This commit is contained in:
parent
86dfceec40
commit
7fbbd5f4a1
6 changed files with 84 additions and 113 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- `/arminsayshi` now plays a 3D version in fullscreen mode, with one cube per pixel of Armin. The 3D pi logo easter egg on header logo click is replaced by it.
|
||||
|
||||
## [1.0.0] - 2026-10-01
|
||||
|
||||
### New Features
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { type TUI, TuiAltScreen } from "@earendil-works/pi-tui";
|
||||
|
||||
/**
|
||||
* Plays the 3D Armin easter egg (see armin-3d.ts), which loads on first use. Only fullscreen mode can show it,
|
||||
* because it dissolves the rendered screen. The screen is captured before loading. Returns false when it cannot
|
||||
* play, so the caller can fall back to the inline version.
|
||||
*/
|
||||
export function playArmin3d(tui: TUI): boolean {
|
||||
if (!(tui instanceof TuiAltScreen)) return false;
|
||||
if (tui.hasOverlay()) return true;
|
||||
const screen = tui.getScreenLines();
|
||||
import("./armin-3d.ts").then(
|
||||
(module) => module.playArmin3d(tui, screen),
|
||||
() => {},
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -13,15 +13,15 @@ import {
|
|||
visibleWidth,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { ARMIN_HEIGHT, ARMIN_WIDTH, isArminPixel } from "./armin.ts";
|
||||
import { formatKeyText } from "./keybinding-hints.ts";
|
||||
|
||||
/**
|
||||
* Fullscreen easter egg shown when the header logo is clicked. The current screen dissolves into braille dust,
|
||||
* spreading out from the logo, while the logo lifts off as a 3D object, flies to the center, grows, and spins.
|
||||
* Leaving plays the same timeline backwards, so the logo lands on the header and the screen reassembles.
|
||||
* Fullscreen 3D version of the /arminsayshi easter egg. The current screen dissolves into braille dust, spreading
|
||||
* out from the center, while Armin's bitmap, built from one cube per pixel, grows out of the center and spins.
|
||||
* Leaving plays the same timeline backwards, so Armin shrinks away and the screen reassembles.
|
||||
*
|
||||
* The logo is ray cast per braille dot. A braille cell holds 2x4 dots and roughly square dots, so one logo
|
||||
* pixel (a half block) is exactly 2x2 dots and the header logo (4x2 cells) is 8x8 dots at the start.
|
||||
* The cubes are ray cast per braille dot. A braille cell holds 2x4 roughly square dots.
|
||||
*/
|
||||
|
||||
type Rgb = readonly [number, number, number];
|
||||
|
|
@ -55,7 +55,6 @@ interface Star {
|
|||
interface LogoBox {
|
||||
min: readonly [number, number, number];
|
||||
max: readonly [number, number, number];
|
||||
color: Rgb;
|
||||
}
|
||||
|
||||
interface Pose {
|
||||
|
|
@ -68,31 +67,29 @@ interface Pose {
|
|||
roll: number;
|
||||
}
|
||||
|
||||
const CORAL: Rgb = [228, 138, 122];
|
||||
const BLUE: Rgb = [79, 142, 179];
|
||||
const YELLOW: Rgb = [234, 182, 93];
|
||||
|
||||
const DEPTH = 0.7;
|
||||
const CAMERA_DISTANCE = 10;
|
||||
// Far enough that the perspective stays mild for a figure about 36 cubes tall.
|
||||
const CAMERA_DISTANCE = 80;
|
||||
const FLY_START = 0.1;
|
||||
const FLY_DURATION = 1.3;
|
||||
/** Braille dots per pixel when Armin appears, growing from a speck at the center. */
|
||||
const START_SCALE = 0.1;
|
||||
|
||||
/** Grid position of a logo block: column and row in the 4x4 logo, and layer (-1, 0, 1) in depth. */
|
||||
/** Grid position of a block: column and row in the bitmap, and layer (-1, 0, 1) in depth. */
|
||||
type Cell3 = readonly [number, number, number];
|
||||
|
||||
// The logo's pixels on a 4x4 grid. Each pixel is a block that the puzzle slides around.
|
||||
const LOGO_PIXELS = ["ccc.", "b.c.", "bb.y", "b..y"];
|
||||
const BLOCKS: Array<{ home: Cell3; color: Rgb }> = LOGO_PIXELS.flatMap((line, row) =>
|
||||
[...line].flatMap((pixel, column) => {
|
||||
const color = pixel === "c" ? CORAL : pixel === "b" ? BLUE : pixel === "y" ? YELLOW : undefined;
|
||||
return color ? [{ home: [column, row, 0] as Cell3, color }] : [];
|
||||
}),
|
||||
);
|
||||
// One block per foreground pixel of Armin's bitmap. Each is a block that the puzzle slides around.
|
||||
const BLOCKS: Array<{ home: Cell3 }> = [];
|
||||
for (let row = 0; row < ARMIN_HEIGHT; row++) {
|
||||
for (let column = 0; column < ARMIN_WIDTH; column++) {
|
||||
if (isArminPixel(column, row)) BLOCKS.push({ home: [column, row, 0] });
|
||||
}
|
||||
}
|
||||
// Farthest distance of any block corner from the origin, with blocks on the outer depth layers.
|
||||
const LOGO_RADIUS = Math.hypot(2, 2, 1 + DEPTH / 2);
|
||||
const LOGO_RADIUS = Math.hypot(ARMIN_WIDTH / 2, ARMIN_HEIGHT / 2, 1 + DEPTH / 2);
|
||||
|
||||
// The sliding puzzle: after the logo spun for a while, blocks slide into free neighboring cells, one step at
|
||||
// a time. Each cycle shuffles, flies every block back home, and holds the logo briefly.
|
||||
// The sliding puzzle: after Armin spun for a while, blocks slide into free neighboring cells, a few at a time.
|
||||
// Each cycle shuffles, flies every block back home, and holds the figure briefly.
|
||||
const PUZZLE_START = FLY_START + FLY_DURATION + 3;
|
||||
const PUZZLE_STEP = 0.3;
|
||||
const PUZZLE_STEPS = 12;
|
||||
|
|
@ -119,12 +116,12 @@ function shuffleSteps(cycle: number): Cell3[][] {
|
|||
const lastMoved = new Set<number>();
|
||||
let random = cycle * 7_919 + 1;
|
||||
const next = () => hash(random++);
|
||||
const key = ([x, y, z]: Cell3) => (z + 1) * 16 + y * 4 + x;
|
||||
const key = ([x, y, z]: Cell3) => ((z + 1) * ARMIN_HEIGHT + y) * ARMIN_WIDTH + x;
|
||||
for (let step = 0; step < PUZZLE_STEPS; step++) {
|
||||
const occupied = new Set(positions.map(key));
|
||||
const nextPositions = [...positions];
|
||||
const moved = new Set<number>();
|
||||
const moveCount = next() < 0.4 ? 2 : 1;
|
||||
const moveCount = Math.round(BLOCKS.length * (0.03 + next() * 0.04));
|
||||
for (let move = 0; move < moveCount; move++) {
|
||||
const candidates: Array<{ block: number; target: Cell3 }> = [];
|
||||
for (let block = 0; block < positions.length; block++) {
|
||||
|
|
@ -132,7 +129,7 @@ function shuffleSteps(cycle: number): Cell3[][] {
|
|||
const [x, y, z] = positions[block]!;
|
||||
for (const [dx, dy, dz] of PUZZLE_MOVES) {
|
||||
const target: Cell3 = [x + dx, y + dy, z + dz];
|
||||
if (target[0] < 0 || target[0] > 3 || target[1] < 0 || target[1] > 3) continue;
|
||||
if (target[0] < 0 || target[0] >= ARMIN_WIDTH || target[1] < 0 || target[1] >= ARMIN_HEIGHT) continue;
|
||||
if (target[2] < -1 || target[2] > 1 || occupied.has(key(target))) continue;
|
||||
candidates.push({ block, target });
|
||||
}
|
||||
|
|
@ -439,7 +436,7 @@ class LogoRaster {
|
|||
/** Ray parameter of the nearest hit per dot; smaller is nearer. */
|
||||
private depth = new Float32Array(0);
|
||||
/** Face index + 1 of the nearest hit per dot, 0 for none. */
|
||||
private faceIds = new Uint8Array(0);
|
||||
private faceIds = new Uint16Array(0);
|
||||
/** Cells written by the previous frame, cleared before the next one. */
|
||||
private dirty: { minX: number; minY: number; maxX: number; maxY: number } | undefined;
|
||||
/** Cells with a halo from the previous frame, cleared before the next one. */
|
||||
|
|
@ -450,6 +447,7 @@ class LogoRaster {
|
|||
height: number,
|
||||
pose: Pose,
|
||||
boxes: readonly LogoBox[],
|
||||
color: Rgb,
|
||||
background: Rgb,
|
||||
haloStrength: number,
|
||||
): void {
|
||||
|
|
@ -465,7 +463,7 @@ class LogoRaster {
|
|||
this.haloRgb = new Float32Array(width * height * 3);
|
||||
this.haloDirty = undefined;
|
||||
this.depth = new Float32Array(dotWidth * dotHeight).fill(Infinity);
|
||||
this.faceIds = new Uint8Array(dotWidth * dotHeight);
|
||||
this.faceIds = new Uint16Array(dotWidth * dotHeight);
|
||||
this.dirty = undefined;
|
||||
} else if (this.dirty) {
|
||||
const { minX, minY, maxX, maxY } = this.dirty;
|
||||
|
|
@ -490,7 +488,7 @@ class LogoRaster {
|
|||
// The camera sits at (0, 0, CAMERA_DISTANCE) in camera space; object space is the transpose rotation.
|
||||
const origin = [m[6]! * CAMERA_DISTANCE, m[7]! * CAMERA_DISTANCE, m[8]! * CAMERA_DISTANCE];
|
||||
const light = isLight(background);
|
||||
const faces = this.visibleFaces(m, origin, pose, boxes, dotWidth, dotHeight, light);
|
||||
const faces = this.visibleFaces(m, origin, pose, boxes, color, dotWidth, dotHeight, light);
|
||||
if (faces.length === 0) return;
|
||||
let minX = dotWidth;
|
||||
let minY = dotHeight;
|
||||
|
|
@ -672,6 +670,7 @@ class LogoRaster {
|
|||
origin: number[],
|
||||
pose: Pose,
|
||||
boxes: readonly LogoBox[],
|
||||
color: Rgb,
|
||||
dotWidth: number,
|
||||
dotHeight: number,
|
||||
lightBackground: boolean,
|
||||
|
|
@ -750,9 +749,9 @@ class LogoRaster {
|
|||
vMin: box.min[v]!,
|
||||
vMax: box.max[v]!,
|
||||
...face,
|
||||
red: Math.min(255, box.color[0] * light + specular),
|
||||
green: Math.min(255, box.color[1] * light + specular),
|
||||
blue: Math.min(255, box.color[2] * light + specular),
|
||||
red: Math.min(255, color[0] * light + specular),
|
||||
green: Math.min(255, color[1] * light + specular),
|
||||
blue: Math.min(255, color[2] * light + specular),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -761,21 +760,13 @@ class LogoRaster {
|
|||
}
|
||||
}
|
||||
|
||||
export interface PiLogoAnimationOptions {
|
||||
/** The screen to dissolve, as rendered lines. */
|
||||
screen: readonly string[];
|
||||
/** Terminal cell of the header logo's top-left corner. */
|
||||
logoColumn: number;
|
||||
logoRow: number;
|
||||
}
|
||||
|
||||
let playing = false;
|
||||
|
||||
/**
|
||||
* Show the animation as a fullscreen overlay until it is dismissed. The overlay takes focus and mouse input and
|
||||
* returns focus when hidden, so the rest of the UI keeps running underneath untouched.
|
||||
*/
|
||||
export async function playPiLogoAnimation(tui: TUI, options: PiLogoAnimationOptions): Promise<void> {
|
||||
export async function playArmin3d(tui: TUI, screen: readonly string[]): Promise<void> {
|
||||
if (playing) return;
|
||||
playing = true;
|
||||
// Fading needs the terminal's actual default colors; the theme only knows its own.
|
||||
|
|
@ -786,23 +777,26 @@ export async function playPiLogoAnimation(tui: TUI, options: PiLogoAnimationOpti
|
|||
const colors = {
|
||||
foreground: toRgbTuple(reported.foreground, toRgb(theme.colors.text)),
|
||||
background: toRgbTuple(reported.background, dark ? [0, 0, 0] : [255, 255, 255]),
|
||||
armin: toRgb(theme.colors.accent),
|
||||
};
|
||||
if (tui.hasOverlay()) {
|
||||
playing = false;
|
||||
return;
|
||||
}
|
||||
const animation = new PiLogoAnimation(tui, options, colors, () => {
|
||||
const animation = new Armin3d(tui, screen, colors, () => {
|
||||
playing = false;
|
||||
overlay.hide();
|
||||
});
|
||||
const overlay = tui.showOverlay(animation, { anchor: "top-left", width: "100%", maxHeight: "100%" });
|
||||
}
|
||||
|
||||
export class PiLogoAnimation implements Component {
|
||||
export class Armin3d implements Component {
|
||||
private readonly tui: TUI;
|
||||
private readonly options: PiLogoAnimationOptions;
|
||||
/** The screen to dissolve, as rendered lines. */
|
||||
private readonly screen: readonly string[];
|
||||
private readonly foreground: Rgb;
|
||||
private readonly background: Rgb;
|
||||
private readonly color: Rgb;
|
||||
private readonly onDone: () => void;
|
||||
private readonly startTime = performance.now();
|
||||
private lastRender = performance.now();
|
||||
|
|
@ -821,14 +815,15 @@ export class PiLogoAnimation implements Component {
|
|||
|
||||
constructor(
|
||||
tui: TUI,
|
||||
options: PiLogoAnimationOptions,
|
||||
colors: { foreground: Rgb; background: Rgb },
|
||||
screen: readonly string[],
|
||||
colors: { foreground: Rgb; background: Rgb; armin: Rgb },
|
||||
onDone: () => void,
|
||||
) {
|
||||
this.tui = tui;
|
||||
this.options = options;
|
||||
this.screen = screen;
|
||||
this.foreground = colors.foreground;
|
||||
this.background = colors.background;
|
||||
this.color = colors.armin;
|
||||
this.onDone = onDone;
|
||||
this.timer = setInterval(() => {
|
||||
// Also stop when no longer rendered, e.g. when pi hides all overlays on exit.
|
||||
|
|
@ -915,12 +910,12 @@ export class PiLogoAnimation implements Component {
|
|||
|
||||
const boxes: LogoBox[] = BLOCKS.map((block, index) => {
|
||||
const [dx, dy, dz] = offsets[index]!;
|
||||
const x = block.home[0] - 2 + dx;
|
||||
const y = block.home[1] - 2 + dy;
|
||||
return { min: [x, y, dz - DEPTH / 2], max: [x + 1, y + 1, dz + DEPTH / 2], color: block.color };
|
||||
const x = block.home[0] - ARMIN_WIDTH / 2 + dx;
|
||||
const y = block.home[1] - ARMIN_HEIGHT / 2 + dy;
|
||||
return { min: [x, y, dz - DEPTH / 2], max: [x + 1, y + 1, dz + DEPTH / 2] };
|
||||
});
|
||||
const logo = this.logo;
|
||||
logo.render(width, height, pose, boxes, background, isLight(background) ? LIGHT_HALO_STRENGTH : 0);
|
||||
logo.render(width, height, pose, boxes, this.color, background, isLight(background) ? LIGHT_HALO_STRENGTH : 0);
|
||||
const halo = (index: number, base: Rgb | undefined): Rgb | undefined => {
|
||||
const amount = logo.haloAmount[index]!;
|
||||
if (amount <= 0) return base;
|
||||
|
|
@ -1094,17 +1089,12 @@ export class PiLogoAnimation implements Component {
|
|||
}
|
||||
|
||||
private pose(width: number, height: number, progress: number, time: number): Pose {
|
||||
// At the start the front face must cover exactly the header logo's 8x8 dots despite the perspective.
|
||||
const startScale = (2 * (CAMERA_DISTANCE - DEPTH / 2)) / CAMERA_DISTANCE;
|
||||
const reach = LOGO_RADIUS * (CAMERA_DISTANCE / (CAMERA_DISTANCE - LOGO_RADIUS));
|
||||
const endScale = Math.max(startScale, Math.min(width * 2 * 0.35, (height * 4 - 8) * 0.48) / reach);
|
||||
const startX = this.options.logoColumn * 2 + 4;
|
||||
const startY = this.options.logoRow * 4 + 4;
|
||||
const endX = width;
|
||||
const endY = height * 2 - 2;
|
||||
const startScale = START_SCALE;
|
||||
const endScale = Math.max(startScale, Math.min(width * 2 * 0.45, (height * 4 - 8) * 0.48) / reach);
|
||||
return {
|
||||
centerX: startX + (endX - startX) * progress,
|
||||
centerY: startY + (endY - startY) * progress,
|
||||
centerX: width,
|
||||
centerY: height * 2 - 2,
|
||||
// Interpolate the zoom geometrically so it feels uniform.
|
||||
scale: startScale * (endScale / startScale) ** progress,
|
||||
yaw: spinAngle(time),
|
||||
|
|
@ -1152,10 +1142,10 @@ export class PiLogoAnimation implements Component {
|
|||
}
|
||||
|
||||
private prepareCells(width: number, foreground: Rgb): ScreenCell[][] {
|
||||
const cells = parseScreen(this.options.screen, width, foreground, this.background);
|
||||
const centerX = this.options.logoColumn + 2;
|
||||
const centerY = this.options.logoRow + 1;
|
||||
const cells = parseScreen(this.screen, width, foreground, this.background);
|
||||
const height = Math.max(1, this.tui.terminal.rows);
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const farthest = Math.hypot(Math.max(centerX, width - centerX), Math.max(centerY, height - centerY) * 2);
|
||||
for (let row = 0; row < cells.length; row++) {
|
||||
const line = cells[row]!;
|
||||
|
|
@ -1174,20 +1164,6 @@ export class PiLogoAnimation implements Component {
|
|||
cell.ink = glyphInk(owner.text, seed);
|
||||
}
|
||||
}
|
||||
// The 3D logo replaces the header logo.
|
||||
for (let row = this.options.logoRow; row < this.options.logoRow + 2; row++) {
|
||||
const line = cells[row];
|
||||
if (!line) continue;
|
||||
for (let column = this.options.logoColumn; column < this.options.logoColumn + 4; column++) {
|
||||
const cell = line[column];
|
||||
if (cell) {
|
||||
cell.text = " ";
|
||||
cell.width = 1;
|
||||
cell.ink = 0;
|
||||
cell.bg = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
|
|
@ -6,8 +6,10 @@ import type { Component, TUI } from "@earendil-works/pi-tui";
|
|||
import { theme } from "../theme/theme.ts";
|
||||
|
||||
// XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground
|
||||
const WIDTH = 31;
|
||||
const HEIGHT = 36;
|
||||
export const ARMIN_WIDTH = 31;
|
||||
export const ARMIN_HEIGHT = 36;
|
||||
const WIDTH = ARMIN_WIDTH;
|
||||
const HEIGHT = ARMIN_HEIGHT;
|
||||
const BITS = [
|
||||
0xff, 0xff, 0xff, 0x7f, 0xff, 0xf0, 0xff, 0x7f, 0xff, 0xed, 0xff, 0x7f, 0xff, 0xdb, 0xff, 0x7f, 0xff, 0xb7, 0xff,
|
||||
0x7f, 0xff, 0x77, 0xfe, 0x7f, 0x3f, 0xf8, 0xfe, 0x7f, 0xdf, 0xff, 0xfe, 0x7f, 0xdf, 0x3f, 0xfc, 0x7f, 0x9f, 0xc3,
|
||||
|
|
@ -27,7 +29,7 @@ type Effect = "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "
|
|||
const EFFECTS: Effect[] = ["typewriter", "scanline", "rain", "fade", "crt", "glitch", "dissolve"];
|
||||
|
||||
// Get pixel at (x, y): true = foreground, false = background
|
||||
function getPixel(x: number, y: number): boolean {
|
||||
export function isArminPixel(x: number, y: number): boolean {
|
||||
if (y >= HEIGHT) return false;
|
||||
const byteIndex = y * BYTES_PER_ROW + Math.floor(x / 8);
|
||||
const bitIndex = x % 8;
|
||||
|
|
@ -36,8 +38,8 @@ function getPixel(x: number, y: number): boolean {
|
|||
|
||||
// Get the character for a cell (2 vertical pixels packed)
|
||||
function getChar(x: number, row: number): string {
|
||||
const upper = getPixel(x, row * 2);
|
||||
const lower = getPixel(x, row * 2 + 1);
|
||||
const upper = isArminPixel(x, row * 2);
|
||||
const lower = isArminPixel(x, row * 2 + 1);
|
||||
if (upper && lower) return "█";
|
||||
if (upper) return "▀";
|
||||
if (lower) return "▄";
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { type TUI, TuiAltScreen } from "@earendil-works/pi-tui";
|
||||
|
||||
/**
|
||||
* Plays the logo easter egg (see pi-logo-animation.ts), which loads on the first click. Only fullscreen mode
|
||||
* can show it, because it dissolves the rendered screen. The screen is captured before loading.
|
||||
*/
|
||||
export function playPiLogoAnimation(tui: TUI, logoColumn: number, logoRow: number): void {
|
||||
if (!(tui instanceof TuiAltScreen) || tui.hasOverlay()) return;
|
||||
const screen = tui.getScreenLines();
|
||||
import("./pi-logo-animation.ts").then(
|
||||
(module) => module.playPiLogoAnimation(tui, { screen, logoColumn, logoRow }),
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
|
@ -48,8 +48,6 @@ import {
|
|||
type TUI,
|
||||
TuiAltScreen,
|
||||
TuiMainScreen,
|
||||
type TuiMouseEvent,
|
||||
type TuiMouseEventResult,
|
||||
visibleWidth,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
|
@ -134,6 +132,7 @@ import { checkForNewPiVersion, type LatestPiRelease } from "../../utils/version-
|
|||
import { reportBug } from "./bug-report.ts";
|
||||
import { createChatViewport } from "./chat-viewport.ts";
|
||||
import { ArminComponent } from "./components/armin.ts";
|
||||
import { playArmin3d } from "./components/armin-3d.lazy.ts";
|
||||
import { AssistantMessageComponent } from "./components/assistant-message.ts";
|
||||
import { BashExecutionComponent } from "./components/bash-execution.ts";
|
||||
import { BranchSummaryMessageComponent } from "./components/branch-summary-message.ts";
|
||||
|
|
@ -159,7 +158,6 @@ import {
|
|||
OAuthSelectorComponent,
|
||||
} from "./components/oauth-selector.ts";
|
||||
import { piLogoLines, piWordmark, supportsPiLogo } from "./components/pi-logo.ts";
|
||||
import { playPiLogoAnimation } from "./components/pi-logo-animation.lazy.ts";
|
||||
import { createLoginMenuSelector } from "./components/radius-login-selector.ts";
|
||||
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.ts";
|
||||
import { SessionSelectorComponent } from "./components/session-selector.ts";
|
||||
|
|
@ -251,17 +249,6 @@ class ExpandableText extends ThemedText implements Expandable {
|
|||
}
|
||||
}
|
||||
|
||||
/** The built-in header. Clicking its logo (the first two lines, after one column of padding) plays an easter egg. */
|
||||
class BuiltInHeader extends ExpandableText {
|
||||
onLogoClick: ((column: number, row: number) => void) | undefined;
|
||||
|
||||
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
||||
if (event.type !== "click" || event.y > 1 || event.x < 1 || event.x > 4 || !this.onLogoClick) return undefined;
|
||||
this.onLogoClick(event.screenX - event.x + 1, event.screenY - event.y);
|
||||
return { handled: true };
|
||||
}
|
||||
}
|
||||
|
||||
type CompactionQueuedMessage = {
|
||||
text: string;
|
||||
mode: "steer" | "followUp";
|
||||
|
|
@ -1048,15 +1035,13 @@ export class InteractiveMode {
|
|||
);
|
||||
const onboarding = () =>
|
||||
theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
|
||||
const header = new BuiltInHeader(
|
||||
this.builtInHeader = new ExpandableText(
|
||||
() => `${withLogo(compactInstructions())}\n${compactOnboarding()}\n\n${onboarding()}`,
|
||||
() => `${withLogo(expandedInstructions())}\n\n${onboarding()}`,
|
||||
this.getStartupExpansionState(),
|
||||
1,
|
||||
0,
|
||||
);
|
||||
if (showLogo) header.onLogoClick = (column, row) => playPiLogoAnimation(this.renderer, column, row);
|
||||
this.builtInHeader = header;
|
||||
|
||||
// Setup UI layout
|
||||
this.headerContainer.addChild(new Spacer(1));
|
||||
|
|
@ -6891,6 +6876,7 @@ export class InteractiveMode {
|
|||
}
|
||||
|
||||
private handleArminSaysHi(): void {
|
||||
if (playArmin3d(this.renderer)) return;
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new ArminComponent(this.ui));
|
||||
this.ui.requestRender();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue