diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8058403c..0254d098 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed bare `readline` import to use `node:readline` prefix for Deno compatibility ([#2885](https://github.com/badlogic/pi-mono/issues/2885) by [@milosv-vtool](https://github.com/milosv-vtool)) - Fixed auto-retry to treat stream failures like `request ended without sending any chunks` as transient errors ([#2892](https://github.com/badlogic/pi-mono/issues/2892)) +- Fixed interactive startup notices to render after the initial resource listing, and added a bundled Earendil startup announcement with inline image rendering for April 8, 2026. ## [0.65.2] - 2026-04-06 diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 0defc31a..1ce637aa 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -33,8 +33,8 @@ "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets", "build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js --outfile dist/pi && npm run copy-binary-assets", - "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/", - "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/", + "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/", + "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/", "test": "vitest --run", "prepublishOnly": "npm run clean && npm run build" }, diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 5b9e4827..4ed14a02 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -158,6 +158,26 @@ export function getChangelogPath(): string { return resolve(join(getPackageDir(), "CHANGELOG.md")); } +/** + * Get path to built-in interactive assets directory. + * - For Bun binary: assets/ next to executable + * - For Node.js (dist/): dist/modes/interactive/assets/ + * - For tsx (src/): src/modes/interactive/assets/ + */ +export function getInteractiveAssetsDir(): string { + if (isBunBinary) { + return join(dirname(process.execPath), "assets"); + } + const packageDir = getPackageDir(); + const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; + return join(packageDir, srcOrDist, "modes", "interactive", "assets"); +} + +/** Get path to a bundled interactive asset */ +export function getBundledInteractiveAssetPath(name: string): string { + return join(getInteractiveAssetsDir(), name); +} + // ============================================================================= // App Config (from package.json piConfig) // ============================================================================= diff --git a/packages/coding-agent/src/modes/interactive/assets/clankolas.png b/packages/coding-agent/src/modes/interactive/assets/clankolas.png new file mode 100644 index 00000000..d3dc65da Binary files /dev/null and b/packages/coding-agent/src/modes/interactive/assets/clankolas.png differ diff --git a/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts b/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts new file mode 100644 index 00000000..16dc1ef7 --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts @@ -0,0 +1,52 @@ +import * as fs from "node:fs"; +import { Container, Image, Spacer, Text } from "@mariozechner/pi-tui"; +import { getBundledInteractiveAssetPath } from "../../../config.js"; +import { theme } from "../theme/theme.js"; +import { DynamicBorder } from "./dynamic-border.js"; + +const BLOG_URL = "https://mariozechner.at/posts/2026-04-08-ive-sold-out/"; +const IMAGE_FILENAME = "clankolas.png"; + +let cachedImageBase64: string | undefined; +let attemptedImageLoad = false; + +function loadImageBase64(): string | undefined { + if (attemptedImageLoad) { + return cachedImageBase64; + } + + attemptedImageLoad = true; + try { + cachedImageBase64 = fs.readFileSync(getBundledInteractiveAssetPath(IMAGE_FILENAME)).toString("base64"); + } catch { + cachedImageBase64 = undefined; + } + return cachedImageBase64; +} + +export class EarendilAnnouncementComponent extends Container { + constructor() { + super(); + + this.addChild(new DynamicBorder((text) => theme.fg("accent", text))); + this.addChild(new Text(theme.bold(theme.fg("accent", "pi has joined Earendil")), 1, 0)); + this.addChild(new Spacer(1)); + + const imageBase64 = loadImageBase64(); + if (imageBase64) { + this.addChild( + new Image( + imageBase64, + "image/png", + { fallbackColor: (text) => theme.fg("muted", text) }, + { maxWidthCells: 56, filename: IMAGE_FILENAME }, + ), + ); + this.addChild(new Spacer(1)); + } + + this.addChild(new Text(theme.fg("muted", "Read the blog post:"), 1, 0)); + this.addChild(new Text(theme.fg("mdLink", BLOG_URL), 1, 0)); + this.addChild(new DynamicBorder((text) => theme.fg("accent", text))); + } +} diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 10dde29a..27ebf35f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -81,6 +81,7 @@ import { CustomEditor } from "./components/custom-editor.js"; import { CustomMessageComponent } from "./components/custom-message.js"; import { DaxnutsComponent } from "./components/daxnuts.js"; import { DynamicBorder } from "./components/dynamic-border.js"; +import { EarendilAnnouncementComponent } from "./components/earendil-announcement.js"; import { ExtensionEditorComponent } from "./components/extension-editor.js"; import { ExtensionInputComponent } from "./components/extension-input.js"; import { ExtensionSelectorComponent } from "./components/extension-selector.js"; @@ -173,6 +174,7 @@ export class InteractiveMode { private lastSigintTime = 0; private lastEscapeTime = 0; private changelogMarkdown: string | undefined = undefined; + private startupNoticesShown = false; // Status line tracking (for mutating immediately-sequential status updates) private lastStatusSpacer: Spacer | undefined = undefined; @@ -427,6 +429,48 @@ export class InteractiveMode { } } + private shouldShowEarendilAnnouncement(): boolean { + const now = new Date(); + return now.getFullYear() === 2026 && now.getMonth() === 3 && now.getDate() === 8; + } + + private showStartupNoticesIfNeeded(): void { + if (this.startupNoticesShown) { + return; + } + this.startupNoticesShown = true; + + if (this.shouldShowEarendilAnnouncement()) { + if (this.chatContainer.children.length > 0) { + this.chatContainer.addChild(new Spacer(1)); + } + this.chatContainer.addChild(new EarendilAnnouncementComponent()); + } + + if (!this.changelogMarkdown) { + return; + } + + if (this.chatContainer.children.length > 0) { + this.chatContainer.addChild(new Spacer(1)); + } + this.chatContainer.addChild(new DynamicBorder()); + if (this.settingsManager.getCollapseChangelog()) { + const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/); + const latestVersion = versionMatch ? versionMatch[1] : this.version; + const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`; + this.chatContainer.addChild(new Text(condensedText, 1, 0)); + } else { + this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0)); + this.chatContainer.addChild(new Spacer(1)); + this.chatContainer.addChild( + new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()), + ); + this.chatContainer.addChild(new Spacer(1)); + } + this.chatContainer.addChild(new DynamicBorder()); + } + async init(): Promise { if (this.isInitialized) return; @@ -479,37 +523,10 @@ export class InteractiveMode { this.headerContainer.addChild(new Spacer(1)); this.headerContainer.addChild(this.builtInHeader); this.headerContainer.addChild(new Spacer(1)); - - // Add changelog if provided - if (this.changelogMarkdown) { - this.headerContainer.addChild(new DynamicBorder()); - if (this.settingsManager.getCollapseChangelog()) { - const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/); - const latestVersion = versionMatch ? versionMatch[1] : this.version; - const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`; - this.headerContainer.addChild(new Text(condensedText, 1, 0)); - } else { - this.headerContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0)); - this.headerContainer.addChild(new Spacer(1)); - this.headerContainer.addChild( - new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()), - ); - this.headerContainer.addChild(new Spacer(1)); - } - this.headerContainer.addChild(new DynamicBorder()); - } } else { // Minimal header when silenced this.builtInHeader = new Text("", 0, 0); this.headerContainer.addChild(this.builtInHeader); - if (this.changelogMarkdown) { - // Still show changelog notification even in silent mode - this.headerContainer.addChild(new Spacer(1)); - const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/); - const latestVersion = versionMatch ? versionMatch[1] : this.version; - const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`; - this.headerContainer.addChild(new Text(condensedText, 1, 0)); - } } this.ui.addChild(this.chatContainer); @@ -1249,11 +1266,13 @@ export class InteractiveMode { const extensionRunner = this.session.extensionRunner; if (!extensionRunner) { this.showLoadedResources({ extensions: [], force: false }); + this.showStartupNoticesIfNeeded(); return; } this.setupExtensionShortcuts(extensionRunner); this.showLoadedResources({ force: false }); + this.showStartupNoticesIfNeeded(); } private applyRuntimeSettings(): void {