aboutsummaryrefslogtreecommitdiff
path: root/pirc.js
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-06 04:08:49 +0000
committerIgor Soarez <igor@soarez.org>2026-11-06 04:08:49 +0000
commite54cbfa01f23f065b68fb9502a393d98d6ec6af2 (patch)
tree69612826d2cc115c0c66308a1459be634fc1ae33 /pirc.js
parente7329a8ea08bc252a57b1adad2be534174ff59ef (diff)
pirc-extension 0.1.9-dev+c08d7338dad8
Diffstat (limited to 'pirc.js')
-rw-r--r--pirc.js292
1 files changed, 264 insertions, 28 deletions
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 <relay-url> [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);
}