From 19f3c6a0503cefd6a33c296fbc19cf38d3532563 Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Tue, 26 May 2026 09:51:57 -0400 Subject: [PATCH] Add new extensions and updates to the Dev sandbox --- .gitignore | 2 +- .pi/agent/AGENTS.md | 7 ++ .pi/agent/extensions/exit.ts | 40 +++++++ .pi/agent/extensions/quick-question.ts | 143 +++++++++++++++++++++++++ Dockerfile | 20 ++++ 5 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 .pi/agent/extensions/exit.ts create mode 100644 .pi/agent/extensions/quick-question.ts diff --git a/.gitignore b/.gitignore index 2e83fbd..5a69827 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ aws-config .pi/agent/auth.json .pi/agent/sessions/ bin/pi - +.claude/settings.local.json diff --git a/.pi/agent/AGENTS.md b/.pi/agent/AGENTS.md index e1a2d3a..44db8cb 100644 --- a/.pi/agent/AGENTS.md +++ b/.pi/agent/AGENTS.md @@ -32,3 +32,10 @@ Additional notes: - If I send the outputs of failing commands, you should provide recommendations instead of addressing configuration/environment issues yourself. - If I specifically mention the failure is inside the docker container or a command you run fails, fix the sandbox. +## Scope + +Limit your scope to the task at hand. Do touch unrelated code or attempt to fix existing issues. +If you see something just raise it if it is confusing. +Examples: + +- prompt: Write a test for fun1() -> Only write the test for fun1(), don't fix any bugs/issues diff --git a/.pi/agent/extensions/exit.ts b/.pi/agent/extensions/exit.ts new file mode 100644 index 0000000..beba0a7 --- /dev/null +++ b/.pi/agent/extensions/exit.ts @@ -0,0 +1,40 @@ +import type { ExtensionAPI, SessionEntry } from "@earendil-works/pi-coding-agent"; + +const QUICK_MARKER_TYPE = "quick-question-session"; + +type QuickSessionMarker = { + returnSession: string; +}; + +function getQuickSessionMarker(entries: SessionEntry[]): QuickSessionMarker | undefined { + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + if (entry.type !== "custom" || entry.customType !== QUICK_MARKER_TYPE) continue; + + const data = entry.data as Partial | undefined; + if (typeof data?.returnSession === "string") { + return { returnSession: data.returnSession }; + } + } + + return undefined; +} + +export default function (pi: ExtensionAPI) { + pi.on("input", async (event, ctx) => { + if (event.source === "extension") return { action: "continue" }; + if (event.text.trim() !== "exit") return { action: "continue" }; + + const marker = getQuickSessionMarker(ctx.sessionManager.getBranch()); + if (marker) { + (ctx.sessionManager as unknown as { setSessionFile(path: string): void }).setSessionFile(marker.returnSession); + ctx.ui.setStatus("quick-question", undefined); + ctx.ui.setWidget("quick-question", undefined); + ctx.ui.notify("Returned from quick session.", "info"); + return { action: "handled" }; + } + + ctx.shutdown(); + return { action: "handled" }; + }); +} diff --git a/.pi/agent/extensions/quick-question.ts b/.pi/agent/extensions/quick-question.ts new file mode 100644 index 0000000..e908e45 --- /dev/null +++ b/.pi/agent/extensions/quick-question.ts @@ -0,0 +1,143 @@ +import type { ExtensionAPI, ExtensionCommandContext, SessionEntry } from "@earendil-works/pi-coding-agent"; + +const MARKER_TYPE = "quick-question-session"; + +type QuickSessionMarker = { + returnSession: string; + createdAt: string; +}; + +function getQuickSessionMarker(entries: SessionEntry[]): QuickSessionMarker | undefined { + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + if (entry.type !== "custom" || entry.customType !== MARKER_TYPE) continue; + + const data = entry.data as Partial | undefined; + if (typeof data?.returnSession === "string") { + return { + returnSession: data.returnSession, + createdAt: typeof data.createdAt === "string" ? data.createdAt : "", + }; + } + } + + return undefined; +} + +export default function (pi: ExtensionAPI) { + pi.on("input", async (event, ctx) => { + if (event.source === "extension") return { action: "continue" }; + + const aliases: Record = { + "/quick-close": "/quickclose", + "/quick-done": "/quickdone", + "/quick-back": "/quickback", + }; + const replacement = aliases[event.text.trim()]; + if (!replacement) return { action: "continue" }; + + const marker = getQuickSessionMarker(ctx.sessionManager.getBranch()); + if (!marker) { + ctx.ui.notify("This is not a quick question session.", "error"); + return { action: "handled" }; + } + + (ctx.sessionManager as unknown as { setSessionFile(path: string): void }).setSessionFile(marker.returnSession); + ctx.ui.setStatus("quick-question", undefined); + ctx.ui.setWidget("quick-question", undefined); + ctx.ui.notify("Returned from quick session.", "info"); + return { action: "handled" }; + }); + + pi.on("session_start", async (_event, ctx) => { + const marker = getQuickSessionMarker(ctx.sessionManager.getBranch()); + + if (!marker) { + ctx.ui.setStatus("quick-question", undefined); + ctx.ui.setWidget("quick-question", undefined); + return; + } + + ctx.ui.setStatus("quick-question", ctx.ui.theme.fg("accent", "quick")); + ctx.ui.setWidget("quick-question", [ + ctx.ui.theme.fg( + "dim", + "Quick question session. Use /quickclose, /quickdone, or /quickback to return to the original session.", + ), + ]); + }); + + pi.registerCommand("quick", { + description: "Start a temporary one-off question session; pass a question to ask immediately", + handler: async (args, ctx) => { + if (!ctx.hasUI) { + ctx.ui.notify("quick requires interactive mode", "error"); + return; + } + + await ctx.waitForIdle(); + + const returnSession = ctx.sessionManager.getSessionFile(); + if (!returnSession) { + ctx.ui.notify("Current session is ephemeral; cannot return to it later.", "error"); + return; + } + + const question = args.trim(); + + const result = await ctx.newSession({ + parentSession: returnSession, + setup: async (session) => { + session.appendCustomEntry(MARKER_TYPE, { + returnSession, + createdAt: new Date().toISOString(), + } satisfies QuickSessionMarker); + session.appendSessionInfo("Quick question"); + }, + withSession: async (replacementCtx) => { + replacementCtx.ui.notify( + "Quick session started. Use /quickclose, /quickdone, or /quickback to return.", + "info", + ); + + if (question) { + await replacementCtx.sendUserMessage(question); + } else { + replacementCtx.ui.setEditorText(""); + } + }, + }); + + if (result.cancelled) { + ctx.ui.notify("Quick session cancelled", "info"); + } + }, + }); + + async function closeQuickSession(ctx: ExtensionCommandContext) { + await ctx.waitForIdle(); + + const marker = getQuickSessionMarker(ctx.sessionManager.getBranch()); + if (!marker) { + ctx.ui.notify("This is not a quick question session.", "error"); + return; + } + + const result = await ctx.switchSession(marker.returnSession, { + withSession: async (replacementCtx) => { + replacementCtx.ui.notify("Returned from quick session.", "info"); + }, + }); + + if (result.cancelled) { + ctx.ui.notify("Return to original session cancelled", "info"); + } + } + + for (const name of ["quickclose", "quickdone", "quickback", "quick-close", "quick-done", "quick-back"] as const) { + pi.registerCommand(name, { + description: "Close the quick question session and return to the original session", + handler: async (_args, ctx) => closeQuickSession(ctx), + }); + } +} diff --git a/Dockerfile b/Dockerfile index 8afec13..432bfed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ ARG JAVA_VERSION=17 ARG GO_VERSION=1.22.4 ARG GRADLE_VERSION=8.7 ARG TERRAFORM_VERSION=1.8.5 +ARG ZIG_VERSION=0.16.0 ENV DEBIAN_FRONTEND=noninteractive ENV LANG=C.UTF-8 @@ -38,6 +39,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ripgrep \ neovim \ less \ + hugo \ + xz-utils \ && rm -rf /var/lib/apt/lists/* \ && ln -sf /usr/bin/fdfind /usr/local/bin/fd @@ -63,6 +66,10 @@ RUN curl -sSL https://install.python-poetry.org | python3 - && \ # uv (fast Python package/project manager from Astral) RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh +# Keep sandbox-created Python project virtualenvs separate from the host/user .venv. +# This avoids invalid interpreter symlinks when the same workspace is shared with macOS. +ENV UV_PROJECT_ENVIRONMENT=".venv_sandbox" + # Node.js 22 RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - && \ apt-get install -y --no-install-recommends nodejs && \ @@ -112,6 +119,19 @@ RUN ARCH=$(dpkg --print-architecture) && \ unzip /tmp/terraform.zip -d /usr/local/bin && \ rm /tmp/terraform.zip +# Zig +RUN ARCH=$(uname -m) && \ + case "$ARCH" in \ + x86_64) ZIG_ARCH=x86_64 ;; \ + aarch64) ZIG_ARCH=aarch64 ;; \ + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \ + esac && \ + curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}.tar.xz" -o /tmp/zig.tar.xz && \ + tar -C /opt -xf /tmp/zig.tar.xz && \ + mv "/opt/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}" "/opt/zig-${ZIG_VERSION}" && \ + ln -s "/opt/zig-${ZIG_VERSION}/zig" /usr/local/bin/zig && \ + rm /tmp/zig.tar.xz + # Atlassian CLI (acli) RUN mkdir -p -m 755 /etc/apt/keyrings && \ wget -nv -O- https://acli.atlassian.com/gpg/public-key.asc | gpg --dearmor -o /etc/apt/keyrings/acli-archive-keyring.gpg && \