Add new extensions and updates to the Dev sandbox

This commit is contained in:
Alex Selimov 2026-05-26 09:51:57 -04:00
parent 5f053ec81d
commit 19f3c6a050
5 changed files with 211 additions and 1 deletions

2
.gitignore vendored
View file

@ -3,4 +3,4 @@ aws-config
.pi/agent/auth.json
.pi/agent/sessions/
bin/pi
.claude/settings.local.json

View file

@ -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

View file

@ -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<QuickSessionMarker> | 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" };
});
}

View file

@ -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<QuickSessionMarker> | 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<string, string> = {
"/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),
});
}
}

View file

@ -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 && \