From e54cbfa01f23f065b68fb9502a393d98d6ec6af2 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Fri, 6 Nov 2026 04:08:49 +0000 Subject: pirc-extension 0.1.9-dev+c08d7338dad8 --- pirc.js | 292 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 264 insertions(+), 28 deletions(-) (limited to 'pirc.js') diff --git a/pirc.js b/pirc.js index 28dd295..14a6385 100644 --- a/pirc.js +++ b/pirc.js @@ -1,4 +1,4 @@ -// PiRC extension 0.1.9-dev+fb3f6c5449c2. Third-party code in this file: @bufbuild/protobuf@2.16.0, ws@8.22.0; see NOTICE. +// PiRC extension 0.1.9-dev+c08d7338dad8. Third-party code in this file: @bufbuild/protobuf@2.16.0, ws@8.22.0; see NOTICE. import { createRequire as __pircCreateRequire } from "node:module"; const require = __pircCreateRequire(import.meta.url); var __create = Object.create; @@ -3722,8 +3722,6 @@ var require_websocket_server = __commonJS({ // extension/src/index.ts import { randomBytes as randomBytes4 } from "node:crypto"; -import { readFileSync as readFileSync3, realpathSync } from "node:fs"; -import { dirname as dirname3, join as join3 } from "node:path"; // node_modules/.pnpm/@bufbuild+protobuf@2.16.0/node_modules/@bufbuild/protobuf/dist/esm/is-message.js function isMessage(arg, schema) { @@ -4146,12 +4144,12 @@ function compileCreate(desc) { if (property.convert === void 0 || !isObject(initValue)) { message[name] = initValue !== null && initValue !== void 0 ? initValue : {}; } else { - const converted = {}; + const converted2 = {}; const keys = Object.keys(initValue); for (let k = 0; k < keys.length; k++) { - setOwn(converted, keys[k], property.convert(initValue[keys[k]])); + setOwn(converted2, keys[k], property.convert(initValue[keys[k]])); } - message[name] = converted; + message[name] = converted2; } break; case INIT_ONEOF: { @@ -7798,10 +7796,230 @@ function questionNotice(q, number) { return create(NoticeSchema, { number, kind: 3 /* QUESTION */, text: noticeText([q.title, q.message].join(" ")) }); } +// extension/src/photos.ts +import { readFileSync as readFileSync4 } from "node:fs"; +import { dirname as dirname4, join as join3 } from "node:path"; +import { pathToFileURL } from "node:url"; + +// extension/src/pi.ts +import { readFileSync as readFileSync3, realpathSync } from "node:fs"; +import { dirname as dirname3, join as join2 } from "node:path"; +function piPackageDir() { + try { + let dir = dirname3(realpathSync(process.argv[1])); + for (let i = 0; i < 5; i++, dir = dirname3(dir)) { + try { + const pkg = JSON.parse(readFileSync3(join2(dir, "package.json"), "utf8")); + if (pkg.name === "@earendil-works/pi-coding-agent") return dir; + } catch { + } + } + } catch { + } + return void 0; +} +function piVersion() { + const dir = piPackageDir(); + if (!dir) return "unknown"; + try { + return String(JSON.parse(readFileSync3(join2(dir, "package.json"), "utf8")).version); + } catch { + return "unknown"; + } +} + +// extension/src/photos.ts +var PHOTO_ENTRY = "pirc:photos"; +var isTui = (m) => typeof m?.Image === "function" && typeof m.getCapabilities === "function" && typeof m.imageFallback === "function" && typeof m.getImageDimensions === "function"; +var tui; +var convertToPng; +var loading; +function tuiEntry(pi) { + const dirs = [join3(dirname4(pi), "tui")]; + for (let d = pi; ; d = dirname4(d)) { + dirs.push(join3(d, "node_modules", "@earendil-works", "pi-tui")); + if (dirname4(d) === d) break; + } + for (const dir of dirs) { + try { + const pkg = JSON.parse(readFileSync4(join3(dir, "package.json"), "utf8")); + return join3(dir, pkg.exports?.["."]?.import ?? pkg.main ?? "dist/index.js"); + } catch { + } + } + return void 0; +} +async function byName(name) { + try { + return await import(name); + } catch { + return void 0; + } +} +async function byPath(path) { + try { + return { module: await import(pathToFileURL(path).href) }; + } catch (e) { + return { error: e?.message ?? String(e) }; + } +} +function loadPhotos(given = {}, pi = piPackageDir()) { + loading ??= (async () => { + const from = []; + let problem; + const named = isTui(given.tui) ? void 0 : await byName("@earendil-works/pi-tui"); + if (isTui(given.tui)) { + tui = given.tui; + from.push("pi-tui from pi's loader"); + } else if (isTui(named)) { + tui = named; + from.push("pi-tui by name"); + } else { + const entry = pi && tuiEntry(pi); + const got = entry ? await byPath(entry) : { error: pi ? `not found beside pi in ${pi}` : "pi's install was not found from its command line" }; + if (isTui(got.module)) { + tui = got.module; + from.push(`pi-tui from ${entry}`); + } else { + problem = `pi-tui did not load (${got.error ?? `${entry} lacks Image`}), so no photo is drawn`; + } + } + if (typeof given.convertToPng === "function") { + convertToPng = given.convertToPng; + from.push("conversion from pi's loader"); + } else { + const named2 = await byName("@earendil-works/pi-coding-agent"); + const got = typeof named2?.convertToPng === "function" || !pi ? { module: named2 } : await byPath(join3(pi, "dist", "utils", "image-convert.js")); + const fn = got.module?.convertToPng; + if (typeof fn === "function") { + convertToPng = fn; + from.push(named2 === got.module ? "conversion by name" : "conversion from pi's install"); + } else if (!problem) { + problem = `pi's conversion to PNG did not load (${got.error ?? "no convertToPng"}), so in terminals that speak kitty's protocol a photo shows as a line of text`; + } + } + return { problem, from: from.join(", ") || "nothing" }; + })(); + return loading; +} +function photosOf(message) { + const content = message?.role === "user" ? message.content : void 0; + if (!Array.isArray(content)) return []; + return content.filter((c) => c?.type === "image" && typeof c.data === "string" && c.data !== "" && typeof c.mimeType === "string"); +} +var pngs = /* @__PURE__ */ new WeakMap(); +var converting = /* @__PURE__ */ new WeakSet(); +var converted = () => { +}; +function onConverted(fn) { + converted = fn; +} +var needsPng = (p) => tui?.getCapabilities().images === "kitty" && p.mimeType !== "image/png"; +function preparePhotos(photos) { + for (const p of photos) { + if (!convertToPng || !needsPng(p) || pngs.has(p) || converting.has(p)) continue; + converting.add(p); + convertToPng(p.data, p.mimeType).then( + (png) => pngs.set(p, png), + () => pngs.set(p, null) + ).finally(() => { + converting.delete(p); + converted(); + }); + } +} +var Photos = class { + constructor(messageId, find, shown2) { + this.messageId = messageId; + this.find = find; + this.shown = shown2; + } + messageId; + find; + shown; + photos; + drawn = /* @__PURE__ */ new Map(); + show(shown2) { + if (shown2.width !== this.shown.width) this.drawn.clear(); + this.shown = shown2; + this.invalidate(); + } + render(width) { + if (!this.photos) { + const m = this.find(this.messageId); + if (m === void 0) return []; + this.photos = photosOf(m); + } + const lines = []; + for (const p of this.photos) { + const c = this.component(p); + if (!c) continue; + if (lines.length > 0) lines.push(""); + lines.push(...c.render(width)); + } + return lines; + } + invalidate() { + for (const c of this.drawn.values()) c.invalidate(); + } + component(p) { + const t = tui; + const line = () => ({ + render: (width) => { + const s = this.shown.color(t.imageFallback(p.mimeType, t.getImageDimensions(p.data, p.mimeType) ?? void 0)); + return [t.truncateToWidth ? t.truncateToWidth(s, width) : s]; + }, + invalidate() { + } + }); + if (!this.shown.show) return line(); + let image = p; + if (needsPng(p)) { + if (!convertToPng) return line(); + const png = pngs.get(p); + if (png === void 0) { + preparePhotos([p]); + return void 0; + } + if (png === null) return line(); + image = png; + } + let c = this.drawn.get(p); + if (!c) { + c = new t.Image(image.data, image.mimeType, { fallbackColor: this.shown.color }, { maxWidthCells: this.shown.width }); + this.drawn.set(p, c); + } + return c; + } +}; +function shown(settings, color) { + const width = settings?.terminal?.imageWidthCells; + return { + show: settings?.terminal?.showImages ?? true, + width: typeof width === "number" && Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 60, + color + }; +} +function photoRenderer(find, settings) { + const made = /* @__PURE__ */ new WeakMap(); + return (entry, _options, theme) => { + if (!tui || typeof entry.data?.message !== "string") return void 0; + const now = shown(settings(), (s) => theme.fg("dim", s)); + let c = made.get(entry); + if (c) { + c.show(now); + return c; + } + c = new Photos(entry.data.message, find, now); + made.set(entry, c); + return c; + }; +} + // extension/src/launch.ts import { existsSync, statSync } from "node:fs"; import { homedir as homedir2 } from "node:os"; -import { join as join2 } from "node:path"; +import { join as join4 } from "node:path"; // extension/src/place.ts import { execFile } from "node:child_process"; @@ -8012,7 +8230,7 @@ function launchEnv(env, launchId) { function launchFolder(cwd, home = homedir2()) { const given = cwd.trim(); if (!given) return { problem: "no folder given" }; - const path = given === "~" ? home : given.startsWith("~/") ? join2(home, given.slice(2)) : given; + const path = given === "~" ? home : given.startsWith("~/") ? join4(home, given.slice(2)) : given; if (!path.startsWith("/")) return { problem: `not a full path: ${given}` }; try { if (!statSync(path).isDirectory()) return { problem: `not a folder: ${path}` }; @@ -8476,6 +8694,9 @@ var Live = class { for (const item of messageItems(m, { entryId: entryId ?? "", turnId: this.turnId, at }, p)) { this.emitItem(item); } + if (p.requestId && entryId && photosOf(p.message).length > 0) { + this.pi.appendEntry(PHOTO_ENTRY, { message: entryId }); + } } clearTimeout(this.pendingTimer); this.pendingTimer = void 0; @@ -8531,6 +8752,7 @@ var Live = class { if (m.role === "user") { p.requestId = this.matchPrompt(textOf(m.content), Array.isArray(m.content) && m.content.some((c) => c.type === "image")); if (p.requestId) this.confirm((u) => u.requestId === p.requestId); + if (p.requestId) preparePhotos(photosOf(m)); } if (m.role === "assistant") { p.messageId = this.assistantId; @@ -8938,7 +9160,7 @@ var Live = class { pastSessions(requestId, all) { const budget = this.sink.maxPayloadBytes() - 1024; const current = this.ctx.sessionManager.getSessionFile(); - const shown = all.filter((s) => s.path !== current).map((s) => ({ + const shown2 = all.filter((s) => s.path !== current).map((s) => ({ path: s.path, sessionId: s.id, cwd: s.cwd, @@ -8948,8 +9170,8 @@ var Live = class { modifiedAt: timestampFromDate(s.modified), messageCount: s.messageCount })); - return fitting(shown.slice().reverse(), budget, (kept) => create(SessionMessageSchema, { - body: { case: "pastSessions", value: { requestId, sessions: kept.slice().reverse(), truncated: kept.length < shown.length } } + return fitting(shown2.slice().reverse(), budget, (kept) => create(SessionMessageSchema, { + body: { case: "pastSessions", value: { requestId, sessions: kept.slice().reverse(), truncated: kept.length < shown2.length } } })); } // The conversation on the current branch, newest last, within one frame. @@ -9375,6 +9597,18 @@ var Host = class _Host { } catch { } } + // An entry of the session the extension has, through whichever load of + // it has the session (bind): a photo's message. + entry(id) { + return this.live?.ctx.sessionManager.getEntry(id); + } + // Has pi draw its screen again, for a photo's PNG made after pi drew its + // entry. pi gives an entry's renderer no way to ask for that, but setting + // the footer status always redraws, so the status is set again as it is + // (docs/design.md, section 8). + redraw() { + this.showStatus(); + } // --- Sink ------------------------------------------------------------- routed(session, clientKey, m) { this.conn?.send(create(FrameSchema, { @@ -9481,22 +9715,8 @@ var Host = class _Host { }; // extension/src/index.ts -var EXTENSION_VERSION = true ? "0.1.9-dev+fb3f6c5449c2" : "dev"; -function piVersion() { - try { - let dir = dirname3(realpathSync(process.argv[1])); - for (let i = 0; i < 5; i++, dir = dirname3(dir)) { - try { - const pkg = JSON.parse(readFileSync3(join3(dir, "package.json"), "utf8")); - if (pkg.name === "@earendil-works/pi-coding-agent") return String(pkg.version); - } catch { - } - } - } catch { - } - return "unknown"; -} -var LOAD = true ? "0.1.9-dev+fb3f6c5449c2" : `dev ${randomBytes4(4).toString("hex")}`; +var EXTENSION_VERSION = true ? "0.1.9-dev+c08d7338dad8" : "dev"; +var LOAD = true ? "0.1.9-dev+c08d7338dad8" : `dev ${randomBytes4(4).toString("hex")}`; var KEY = /* @__PURE__ */ Symbol.for("pirc.extension"); var proc = globalThis[KEY] ??= {}; if (!proc.host || proc.load !== LOAD) { @@ -9504,7 +9724,7 @@ if (!proc.host || proc.load !== LOAD) { proc.load = LOAD; } var USAGE = "usage: /pirc [status | enrol [token] | reconnect]"; -function index_default(pi) { +async function index_default(pi) { let active = false; const on = (where, fn) => (e) => { const live = proc.host.liveFor(pi); @@ -9515,12 +9735,28 @@ function index_default(pi) { proc.host.fail(where, err); } }; + const photos = await loadPhotos(globalThis[/* @__PURE__ */ Symbol.for("pirc.piParts")]); + pi.registerEntryRenderer(PHOTO_ENTRY, photoRenderer( + (id) => { + const e = proc.host.entry?.(id); + return e?.type === "message" ? e.message : void 0; + }, + () => pi.getSettings() + )); + onConverted(() => proc.host.redraw()); pi.on("session_start", (e, ctx) => { if (ctx.mode !== "tui") return; active = true; try { + if (photos.problem && !proc.photosProblem) { + proc.photosProblem = photos.problem; + proc.host.fail("photos", new Error(photos.problem)); + ctx.ui.notify(`pirc: clients' photos cannot be shown in this pi's chat: ${photos.problem}`, "warning"); + } proc.host.dialogs.install(ctx.ui, () => proc.host.dialogs); proc.host.bind(pi, ctx, e); + proc.host.log(`photos: ${photos.from}`); + proc.host.redraw(); } catch (err) { proc.host.fail("session_start", err); } -- cgit v1.2.3