Switch firstmate to OpenCode 2 and refresh nix-darwin pins
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.
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user