Point fm at the isolated oc2 wrapper, upgrade Homebrew packages on rebuild, and declare ffmpeg, docker-desktop, puremac, utm, node, and tea. Update nixpkgs, home-manager, and nix-homebrew locks. Track Pi agent config, crew-dispatch, and the Gitea PR helper that home.nix already expected.
158 lines
6.6 KiB
TypeScript
158 lines
6.6 KiB
TypeScript
// Pi Calm - a standalone conversation-presentation toggle for Pi.
|
|
//
|
|
// Adapted from the Firstmate project's Calm implementation.
|
|
// Copyright (c) 2026 Kun Chen. MIT License - see the LICENSE file in this directory.
|
|
//
|
|
// Verified against Pi 0.82.0, which exports its shared tool-row component,
|
|
// session_start replacement reasons, agent_start
|
|
// and agent_settled, ExtensionUIContext.setToolsExpanded(), setWorkingVisible(),
|
|
// setWidget() with a disposable component factory, and setHiddenThinkingLabel().
|
|
// ./lib/working-ship.ts owns the animated working presentation this file
|
|
// installs. ./lib/preference.ts owns the local state file. The collapsed-thinking
|
|
// presentation adapter probes the exact public API seam it patches and degrades
|
|
// independently with one clear diagnostic (see installCalmPresentationAdapter
|
|
// below) if a future Pi removes it. The shared tool-row adapter is limited to
|
|
// Pi's seven known built-in names, so generic custom tools and unsupported
|
|
// transcript classes deliberately stay visible.
|
|
//
|
|
// Calm changes presentation only. It never intercepts, transforms, reroutes,
|
|
// removes, or reorders semantic input, tool execution, model context, session
|
|
// storage, or export data; /export and /share render the complete stock
|
|
// transcript.
|
|
import { type ExtensionAPI, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
import { getKeybindings } from "@earendil-works/pi-tui";
|
|
import { installCalmBuiltInToolShellLayout } from "./lib/built-in-tool-shells.ts";
|
|
import { installCalmCollapsedThinkingLayout } from "./lib/collapsed-thinking.ts";
|
|
import { loadCalmPreference, persistCalmPreference } from "./lib/preference.ts";
|
|
import {
|
|
calmPresentationIsActive,
|
|
setCalmPresentation,
|
|
setCalmStockExportRendering,
|
|
} from "./lib/visibility.ts";
|
|
import {
|
|
CALM_WORKING_SHIP_WIDGET_KEY,
|
|
createCalmWorkingShipAnimation,
|
|
createCalmWorkingShipWidget,
|
|
} from "./lib/working-ship.ts";
|
|
|
|
// Each presentation adapter probes the exact Pi API it patches. If a future Pi
|
|
// removes that API, only the affected adapter degrades; the rest of Calm keeps
|
|
// working.
|
|
function installCalmPresentationAdapter(name: string, install: () => void): void {
|
|
try {
|
|
install();
|
|
} catch (error) {
|
|
const reason = error instanceof Error ? error.message : String(error);
|
|
console.error(`Pi Calm: ${name} presentation adapter unavailable, skipping. ${reason}`);
|
|
}
|
|
}
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
installCalmPresentationAdapter("collapsed-thinking", installCalmCollapsedThinkingLayout);
|
|
installCalmPresentationAdapter("built-in-tool-shells", installCalmBuiltInToolShellLayout);
|
|
|
|
let removeTerminalInputHandler: (() => void) | undefined;
|
|
// One logical agent run, tracked from agent_start through agent_settled rather
|
|
// than from turns or tool calls, so the boat never flickers between tool calls,
|
|
// automatic continuations, retries, or compaction that stay inside the same run.
|
|
let agentRunActive = false;
|
|
let workingShipShown = false;
|
|
// One animation instance per extension lifetime. Hiding the working widget
|
|
// freezes this state; the next working period resumes it. session_start resets
|
|
// it so a fresh Pi session starts at the normal initial position. Never
|
|
// module-global.
|
|
const workingShipAnimation = createCalmWorkingShipAnimation();
|
|
|
|
// Single owner of Calm's working-row presentation choice. The widget is only
|
|
// created or removed on a real transition, so repeated starts cannot duplicate
|
|
// its timer.
|
|
const applyWorkingPresentation = (
|
|
ui: ExtensionUIContext,
|
|
forceStockVisibility = false,
|
|
): void => {
|
|
const showShip = agentRunActive && calmPresentationIsActive();
|
|
if (showShip !== workingShipShown) {
|
|
workingShipShown = showShip;
|
|
ui.setWidget(
|
|
CALM_WORKING_SHIP_WIDGET_KEY,
|
|
showShip
|
|
? (tui) => createCalmWorkingShipWidget(tui, workingShipAnimation)
|
|
: undefined,
|
|
);
|
|
ui.setWorkingVisible(!showShip);
|
|
} else if (forceStockVisibility && !showShip) {
|
|
ui.setWorkingVisible(true);
|
|
}
|
|
};
|
|
|
|
pi.on("session_start", (_event, ctx) => {
|
|
setCalmPresentation(loadCalmPreference());
|
|
setCalmStockExportRendering(false);
|
|
agentRunActive = false;
|
|
workingShipShown = false;
|
|
// A genuine new session lifetime starts the boat at the normal initial position.
|
|
workingShipAnimation.reset();
|
|
applyWorkingPresentation(ctx.ui, true);
|
|
ctx.ui.setHiddenThinkingLabel(calmPresentationIsActive() ? "" : undefined);
|
|
removeTerminalInputHandler?.();
|
|
removeTerminalInputHandler = ctx.ui.onTerminalInput((data) => {
|
|
if (!getKeybindings().matches(data, "tui.input.submit")) return;
|
|
|
|
const input = ctx.ui.getEditorText().trim();
|
|
if (
|
|
input !== "/share" &&
|
|
input !== "/export" &&
|
|
!input.startsWith("/export ")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// /export and /share render through the same tool renderers the transcript
|
|
// uses, so force stock output for the duration of the command. Session and
|
|
// export data are never filtered; this only concerns the visual components.
|
|
setCalmStockExportRendering(true);
|
|
setTimeout(() => {
|
|
setCalmStockExportRendering(false);
|
|
const expanded = ctx.ui.getToolsExpanded();
|
|
ctx.ui.setToolsExpanded(!expanded);
|
|
ctx.ui.setToolsExpanded(expanded);
|
|
}, 0);
|
|
});
|
|
});
|
|
|
|
pi.on("agent_start", (_event, ctx) => {
|
|
agentRunActive = true;
|
|
applyWorkingPresentation(ctx.ui);
|
|
});
|
|
|
|
// agent_settled is emitted from a finally block, so it also covers abort and failure.
|
|
pi.on("agent_settled", (_event, ctx) => {
|
|
agentRunActive = false;
|
|
applyWorkingPresentation(ctx.ui);
|
|
});
|
|
|
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
agentRunActive = false;
|
|
applyWorkingPresentation(ctx.ui);
|
|
});
|
|
|
|
pi.registerCommand("calm", {
|
|
description: "Toggle Calm: hide collapsed thinking and built-in tool shells from the transcript (presentation only).",
|
|
handler: async (_args, ctx) => {
|
|
const active = !calmPresentationIsActive();
|
|
// Persist first: if the state file cannot be written, the toggle fails
|
|
// with a clear error instead of silently reverting on the next restart.
|
|
persistCalmPreference(active);
|
|
setCalmPresentation(active);
|
|
applyWorkingPresentation(ctx.ui, true);
|
|
ctx.ui.setHiddenThinkingLabel(active ? "" : undefined);
|
|
|
|
// Flip expansion twice to force a transcript redraw while preserving the
|
|
// user's exact Ctrl+O tools-expanded state.
|
|
const expanded = ctx.ui.getToolsExpanded();
|
|
ctx.ui.setToolsExpanded(!expanded);
|
|
ctx.ui.setToolsExpanded(expanded);
|
|
},
|
|
});
|
|
}
|